Принцип пид регулирования. ТАУ для самых маленьких: пример реализации ПИД-регулятора в Unity3D

В данном разделе приведены описания алгоритмов работы и непрерывных П-, ПИ-, ПД-, ПИД-регуляторов с различными структурами выходного сигнала - аналоговым выходом, дискретным (импульсным) выходом или ШИМ-выходом (широтно импульсным модулированным сигналом).

Структурные схемы непрерывных регуляторов

В данном разделе приведены структурные схемы непрерывных регуляторов с аналоговым выходом -рис.2, с импульсным выходом - рис.3 и с ШИМ (широтно импульсным модулированным) выходом -рис.4.

В процессе работы система автоматического регулирования АР (регулятор) сравнивает текущее значение измеряемого параметра Х, полученного от датчика Д, с заданным значением (заданием SP) и устраняет рассогласование регулирования E (B=SP-PV). Внешние возмущающие воздействия Z также устраняются регулятором. Работа приведенных структурных схем отличается методом формирования выходного управляющего сигнала регулятора.

Непрерывный регулятор с аналоговым выходом

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

Выход Y регулятора АР (например, сигнал 0-20мА, 4-20мА, 0-5мА или 0-10В) воздействует через электропневматический Е/Р сигналов (например, с выходным сигналом 20-100кПа) или электропневматический позиционный регулятор на исполнительный элемент К (регулирующий орган).

Рисунок 2 - Структурная схема регулятора с аналоговым выходом

где:
АР - непрерывный ПИД-регулятор с аналоговым выходом,



Д - датчик,
НП - нормирующий преобразователь (в современных регуляторах является входным устройством)
Y - выходной аналоговый управляющий сигнал Е/Р - электропневматический преобразователь,

Непрерывный регулятор с импульсным выходом

Структурная схема непрерывного регулятора с импульсным выходом приведена на рис.3.

Выходные управляющие сигналы регулятора - сигналы Больше и Меньше (транзистор, реле, симистор) через контактные или бесконтактные управляющие устройства (П) воздействуют на исполнительный элемент К (регулирующий орган).

Рисунок 3 - Структурная схема регулятора с импульсным выходом

где:
АР - непрерывный ПИД-регулятор с импульсным выходом,
SP - узел формирования заданной точки,
PV=X- регулируемый технологический параметр,
Е - рассогласование регулятора,
Д - датчик,
НП - нормирующий преобразователь (в современных регуляторах является входным устройством) ИМП - импульсный ШИМ модулятор, преобразующий выходной сигнал Y в последовательность импульсов со скважностью, пропорциональной выходному сигналу: Q=\Y\/100. Сигналы Больше и Меньше - управляющие воздействия,

К - клапан регулирующий (регулирующий орган).

Непрерывный регулятор с ШИМ (широтно импульсным модулированным) выходом

Структурная схема непрерывного регулятора с ШИМ (широтно импульсным модулированным) выходом приведена на рис.4.

Выходной управляющий сигнал регулятора (транзистор, реле, симистор) через контактные или бесконтактные управляющие устройства (П) воздействуют на исполнительный элемент К (регулирующий орган).

Непрерывные регуляторы с ШИМ выходом широко применяются в системах регулирования температуры, где выходной управляющий симисторный элемент (или твердотельное реле, пускатель) воздействуют на термоэлектрический нагреватель ТЭН, или вентилятор.

Рисунок 4 - Структурная схема регулятора с ШИМ выходом

АР - непрерывный ПИД-регулятор с импульсным ШИМ выходом,
SP - узел формирования заданной точки,
PV=X- регулируемый технологический параметр,
Е - рассогласование регулятора,
Д - датчик,
НП - нормирующий преобразователь (в современных регуляторах является входным устройством) ШИМ - импульсный ШИМ модулятор, преобразующий выходной сигнал Y в последовательность импульсов со скважностью, пропорциональной выходному сигналу: Q=\Y\/100.
П - пускатель контактный или бесконтактный,
К - клапан регулирующий (регулирующий орган).

Согласование выходных устройств непрерывных регуляторов

В ыходной сигнал регулятора должен быть согласован с исполнительным механизмом и исполнительным устройством.

В соответствии с видом привода и исполнительным механизмом необходимо использовать выходное устройство непрерывного регулятора соответствующего типа, см. таблицу 1.

Таблица 1 - Согласование выходных устройств непрерывных регуляторов

Выходное устройство непрерывного регулятора Тип выходного устройства Исполнительный механизм или устройство Вид привода Регулирующий орган
Аналоговый выход ЦАП с выходом 0-5мА, 0-20мА, 4-20мА, 0-10В П-, ПИ-,ПД-, ПИД-закон Преобразователи и позиционные регуляторы электро-пневматические и гидравлические Пневматические исполнительные приводы (с сжатым воздухом в качестве вспомогательной энергии) и электропневматические преобразователи сигналов или электропневматические позиционные регуляторы, электрические (частотные привода)
Импульсный выход Транзистор, реле, симистор П-, ПИ-, ПД-, ПИД-закон Электрические приводы (с редуктором), в т. ч. реверсивные
ШИМ выход Транзистор, реле, симистор П-, ПИ-, ПД-, ПИД-закон Контактные (реле) и бесконтактные (симисторные) пускатели Термоэлектрический нагреватель(ТЭН) и др.

Реакция регулятора на единичное ступенчатое воздействие

Если на вход регулятора подается скачкообразная функция изменения заданной точки - см. рис. 5, то на выходе регулятора возникает реакция на единичное ступенчатое воздействие в соответствии с характеристикой регулятора в функции времени.

Сегодняшняя статья будет посвящена такой замечательной вещи, как . По определению, пропорционально-интегрально-дифференциальный регулятор - устройство в цепи обратной связи, используемое в системах автоматического управления для поддержания заданного значения измеряемого параметра. Чаще всего можно встретить примеры, где ПИД-регулятор используется для регулировки температуры, и, на мой взгляд, этот пример прекрасно подходит для изучения теории и понимания принципа работы регулятора. Поэтому именно задачу регулировки температуры и будем сегодня рассматривать.

Итак, что у нас имеется?

Во-первых, объект, температуру которого необходимо поддерживать на заданном уровне, кроме того, эту температуру необходимо регулировать извне. Во-вторых, наше устройство на базе микроконтроллера, с помощью которого мы и будем решать поставленную задачу. Кроме того, у нас есть измеритель температуры (он сообщит контроллеру текущую температуру) и какое-нибудь устройство для управления мощностью нагревателя. Ну и поскольку необходимо как-то задавать температуру, подключим микроконтроллер к ПК.

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

И для такой задачи, да и вообще любой похожей задачи, отличным решением будет использование пропорционально-интегрально-дифференциального регулятора 😉

Пропорциональная составляющая.

Здесь все просто, берем значение нужной нам температуры (уставку) и вычитаем из него значение текущей температуры. Получаем рассогласование (невязку). Умножаем полученную невязку на коэффициент и получаем значение мощности, которое и передаем на нагреватель. Вот и все) Но при использовании только пропорциональной составляющей есть два больших минуса – во-первых, эффект от нашего воздействия наступает не моментально, а с запаздыванием, и, во-вторых, пропорциональная составляющая никак не учитывает воздействие окружающей среды на объект. Например, когда мы добились того, чтобы температуры объекта была равна нужному нам значению, невязка стала равна нулю, а вместе с ней и выдаваемая мощность стала нулевой. Но температура не может просто так оставаться постоянной, поскольку происходит теплообмен с окружающей средой и объект охлаждается. Таким образом, при использовании только пропорциональной составляющей температура будет колебаться около нужного нам значения.

Давайте разбираться, как ПИД-регулятор решает две выявленные проблемы)

Для решения первой используется дифференциальная составляющая . Она противодействует предполагаемым отклонениям регулируемой величины, которые могут произойти в будущем. Каким образом? Сейчас разберемся!

Итак, пусть у нас текущая температура меньше нужного нам значения. Пропорциональная составляющая начинает выдавать мощность и нагревать объект. Дифференциальная составляющая вносит свой вклад в мощность и представляет из себя производную невязки, взятую также с определенным коэффициентом. Температура растет и приближается к нужному значению, а следовательно невязка в предыдущий момент больше текущего значения невязки, а производная отрицательная. Таким образом, дифференциальная составляющая начинает постепенно снижать мощность до того, как температура достигла необходимого значения. С этим вроде разобрались, вспоминаем про вторую проблему регулятора 😉

А с ней нам поможет справиться интегральная составляющая . Как нам в программе получить интеграл? А легко – просто суммированием (накоплением) значений невязки, на то он и интеграл) Возвращаемся к нашему примеру. Температура ниже значения уставки, начинаем подогревать. Пока мы нагреваем, значение невязки положительное и накапливается в интегральной составляющей. Когда температура “дошла” до нужного нам значения, пропорциональная и дифференциальная составляющая стали равны нулю, а интегральная перестала изменяться, но ее значение не стало равным нулю. Таким образом, благодаря накопленному интегралу мы продолжаем выдавать мощность и нагреватель поддерживает нужную нам температуру, не давая объекту охлаждаться. Вот так вот просто и эффективно =)

В итоге мы получаем следующую формулу ПИД-регулятора:

Тут u(t) – искомое выходное воздействие, а e(t) – значение невязки.

Частенько формулу преображают к следующему виду, но суть от этого не меняется:

Пожалуй, на этом закончим, разобрались мы сегодня как работает ПИД-регулятор, а в ближайшее время разберемся еще и как произвести подбор коэффициентов ПИД-регулятора)

Основная задача контроллера холодильника – поддержание в камере заданной температуры. Делать это будет регулятор температуры за счет изменения электрической мощности на модуле Пельтье.

В предыдущем уроке мы разработали регулятор мощности. Связь регуляторов мощности и температуры выглядит так.

  • Регулятор температуры получает измеренную температуру, сравнивает ее с заданной температурой и вычисляет значение заданной мощности для регулятора мощности.
  • Регулятор мощности формирует ШИМ, соответствующий заданной мощности.

Регулятор мощности мы построили по интегральному закону регулирования. Для стабилизации температуры будем использовать более сложный алгоритм управления – пропорционально-интегрально-дифференцирующий (ПИД) регулятор.

ПИД регулятор.

В предыдущем уроке я подробно рассказал об . Подчеркнул его достоинства и недостатки.

Регулятор, работающий по такому принципу, обладает высокой точностью. Остальные критерии качества регулирования – быстродействие и устойчивость - у него не на высоте.

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

Именно таким устройством является пропорционально-интегрально-дифференцирующий (ПИД) регулятор. Он формирует выходной сигнал, являющийся суммой трех составляющих с разными передаточными характеристиками. Благодаря этому ПИД регулятор обеспечивает высокое качество регулирования и позволяет оптимизировать управление по отдельным критериям.

В формировании выходного сигнала ПИД регулятора участвуют:

  • Пропорциональная составляющая – значение пропорционально ошибке рассогласования (разности заданного и реального значений регулируемого параметра).
  • Интегрирующая составляющая – интеграл ошибки рассогласования.
  • Дифференцирующая составляющая – производная ошибки рассогласования.

Математическая форма записи закона ПИД регулятора имеет вид:

o(t) = P + I + D = K p e(t) + K i ∫e(t)dt + K d de(t)/dt

  • o(t) – выходной сигнал;
  • P – пропорциональная составляющая;
  • I – интегрирующая составляющая;
  • D – дифференцирующая составляющая;
  • Kp, Ki, Kd – коэффициенты пропорционального, интегрирующего, дифференцирующего звеньев;
  • e(t) – ошибка рассогласования.

В схематичном виде ПИД регулятор можно представить так.

Структурная схема ПИД регулятора напряжения U выглядит так.

  • Измеренное напряжение Ureal(t) вычитается из заданного Uset.
  • Полученная ошибка рассогласования e(t) поступает на пропорциональное, интегрирующее и дифференцирующее звенья.
  • В результате суммы составляющих получается управляющее воздействие o(t), которое подается на регулирующий элемент.

При программной реализации ПИД регулятора вычисления выходного сигнала происходят через равные промежутки времени. Т.е. регулятор является дискретным по времени. Поэтому, далее я буду употреблять выражения: предыдущее состояние сигнала, предыдущее значение и т.п. Речь идет о состоянии системы в предыдущей точке временной дискретизации.

Составляющие ПИД регулятора.

Еще раз. Выходной сигнал ПИД регулятора это сумма трех составляющих:

  • пропорциональной;
  • интегрирующей;
  • дифференцирующей.

Пропорциональная составляющая.

P(t) = K p * e(t)

Не имеет памяти, т.е. значение выходного сигнала не зависит от предыдущего состояния системы. Просто ошибка рассогласования, умноженная на коэффициент, передается на выход. Выходной сигнал компенсирует отклонение регулируемого параметра. Сигнал тем больше, чем больше ошибка рассогласования. При ошибке равной 0, сигнал на выходе тоже равен 0.

Пропорциональная составляющая не способна компенсировать ошибку полностью. Это видно из формулы. Выходной сигнал в Kp раз больше ошибки. Если ошибка рассогласования равна 0, то и выходной сигнал регулятора равен 0. А тогда и компенсировать нечем.

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

К недостаткам пропорциональных регуляторов следует отнести:

  • наличие статической ошибки регулирования;
  • невысокая устойчивость при увеличении коэффициента.

Есть весомое преимущество:

  • Высокая скорость регулирования. Реакция пропорционального регулятора на ошибку рассогласования ограничена только временем дискретизации системы.

Регуляторы, работающие только по пропорциональному закону, применяют редко.

Главная задача пропорциональной составляющей в ПИД регуляторе – повысить быстродействие.

Интегрирующая составляющая.

I(t) = K i ∫e(t)dt

Пропорциональна интегралу ошибки рассогласования. С учетом временной дискретности регулятора можно написать так:

I(t) = I(t -1) + K i * e(t)

  • I(t-1) – значение I в предыдущей точке временной дискретизации.

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

К недостаткам интегрального регулятора следует отнести:

  • низкое быстродействие;
  • посредственная устойчивость.

Достоинство:

  • Способность полностью компенсировать ошибку рассогласования при любом коэффициенте усиления.

На практике часто используют интегрирующие регуляторы (только интегрирующая составляющая) и пропорционально-интегрирующие (интегрирующая и пропорциональная составляющие).

Главная задача интегрирующего звена в ПИД регуляторе – компенсация статической ошибки, обеспечение высокой точности регулирования.

Дифференцирующая составляющая.

D(t) = K d de(t)/dt

Пропорциональна скорости изменения ошибки рассогласования. Своеобразный показатель ускорения ошибки рассогласования. Дифференцирующая составляющая предсказывает отклонения регулируемого параметра в будущем и противодействует этому отклонению. Как правило, она компенсирует запаздывания воздействия регулятора на объект и повышает устойчивость системы.

С учетом временной дискретности регулятора дифференцирующую составляющую можно вычислить так:

D(t) = K d * (e(t) - e(t -1))

Она показывает, насколько изменилось значение ошибки рассогласования за одну временную единицу дискретности регулятора.

Регуляторов, состоящих из единственного дифференцирующего звена, не бывает.

Главная задача дифференцирующего звена в ПИД регуляторе – повышение устойчивости.

Настройка ПИД регулятора.

Качество регулирования ПИД регуляторов в значительной мере зависит от того, насколько оптимально выбраны коэффициенты. Коэффициенты ПИД регулятора определяются на практике в системе с реальным объектом путем подбора. Существуют разные методики настройки. Я расскажу только об общих принципах.

О качестве регулирования судят по переходной характеристике регулятора. Т.е. по графику изменения регулируемого параметра во времени.

К традиционным пунктам последовательности настройки ПИД регулятора я бы добавил, что, прежде всего, надо определиться какие критерии качества регулирования предпочтительнее.

В предыдущем уроке при разработке регулятора мощности нас в первую очередь интересовали точность и устойчивость. А быстродействие мы даже искусственно снизили. Какие-то регуляторы работают в условиях значительных помех и им важнее устойчивость, от других требуется высокое быстродействие даже в ущерб точности. Критерии оптимизации могут быть разными. В общем случае ПИД регуляторы настраивают для обеспечения всех критериев качества регулирования на высоком уровне.

Составляющие ПИД регулятора настраиваются отдельно.

  • Отключается интегрирующее и дифференцирующее звенья и выбирается коэффициент пропорционального звена. Если регулятор пропорционально-интегрирующий (отсутствует дифференцирующее звено), то добиваются полного отсутствия колебаний на переходной характеристике. При настройке регулятора на высокое быстродействие колебания могут остаться. Их попытается скомпенсировать дифференцирующее звено.
  • Подключается дифференцирующее звено. Его коэффициентом стремятся убрать колебания параметра регулирования. Если не удается, то уменьшают пропорциональный коэффициент.
  • За счет интегрирующего звена убирают остаточную ошибку рассогласования.

Настройка ПИД регулятора носит итерационный характер. Т.е. пункты подбора коэффициентов могут многократно повторяться до тех пор, пока не будет достигнут приемлемый результат.

Благодаря высоким характеристикам и универсальности ПИД регуляторы широко применяются в системах автоматизации производства.

В следующем уроке будем разрабатывать ПИД регулятор температуры.

П-регулятор - Это регулятор, у которого μ пропорционально σ, т.е.μ = – Кσ.

При скачке входной величины σ на значение (–10ºС) затвор регулирующего органа переходит в новое μ - положение скачком (рис.2.10).

Рис.2.10. Закон регулирования П-регулятора.

Достоинство такого регулирования: регулирующий орган быстро перемещается на новое положение, т.е. высокая скорость регулирования (t – время).

Недостаток: имеет место остаточное отклонение, т.е. имеет место некоторая ошибка регулирования.

И-регулятор Это регулятор, у которого μ пропорционально интегралу σ

При скачке входной величины на значение (–10ºС) затвор регулирующего органа медленно переходит в новое положение (рис.2.11).

Рис.2.11. Закон регулирования И-регулятора.

Достоинство:отсутствие остаточного отклонения регулируемого параметра от зад-го знч-я.

Недостаток: низкая скорость рег-я, т.е. затвор в новое положение перемещается медленно.

ПИ-регулятор – это параллельное соединение предыдущих двух регуляторов (П и И - регуляторов). Этот регулятор сочетает положительные моменты П и И -регуляторов. У ПИ-регулятора (рис.2.12) регулирующее воздействие μ перемещает затвор пропорционально отклонению параметра σ и интегралу отклонения σ.

Где: К, Т и – параметры настройки регулятора. Как видим, формула данного закона – это сумма двух предыдущих формул. Затвор регулирующего органа часть пути пройдет скачком по П-закону, а оставшуюся часть – медленно по И - закону.

Рис.2.12. Закон регулирования ПИ-регулятора

Регуляторы с предварением

ПД-регулятор - это такой регулятор (рис.2.13), у которого выходной сигнал μ пропорционален входному сигналу σ и производной dσ/dt, т.е..

Рис.2.13. Закон регулирования ПД-регулятора.

Производная dσ/dt характеризует тенденцию изменения (отклонения) регулируемой величины. Величина и знак воздействия от производной позволяют регулятору как бы предвидеть в какую сторону и на сколько отклонилась бы регулируемая величина под действием данного возмущения. Это предвидение позволяет регулятору предварять своим воздействием возможное отклонение регулируемой величины. В результате процесс регулирования завершается в более короткое время.

Сначала затвор скачком переходит из точки а в точку в (П – закон), т.е. больше чем надо, затем отскакивает назад в точку б (дифференциальное действие), и остаётся в этом положении.

ПИД-регулятор .

У него 3 родителя: П-регулятор, И-регулятор, ПД-регулятор. Соответственно складываются 3 формулы (рис.2.14.)

.

Здесь: К, Т и, Т д – параметры настройки, которые можно настроить вручную.

Рис.2.14. Закон регулирования ПИД-регулятора.

ПИД - закон используется во всех контроллерах. Сначала затвор скачком переходит из точки а в точку в (П – закон), т.е. больше чем надо, затем отскакивает назад в точку б (дифференциальное действие), а далее затвор медленно перемещается в конечное положение (И – закон). В результате процесс регулирования завершается в более короткое время и с меньшей погрешностью регулирования.

Системы автоматического управления (САУ) предназначены для автоматического изменения одного или нескольких параметров объекта управления с целью установления требуемого режима его работы. САУ обеспечивает поддержание постоянства заданных значений регулируемых параметров или их изменение по заданному закону либо оптимизирует определенные критерии качества управления. Например, к таким системам относятся:

  • системы стабилизации,
  • системы программного управления,
  • следящие системы

Это достаточно широкий класс систем, которые можно найти где угодно. Но какое это отношение имеет к Unity3D и вероятно к играм в частности? В принципе прямое: в любой игре так или иначе использующей симуляцию как элемент геймплея реализуются САУ, к таким играм относятся, например, Kerbal Space Programm, Digital Combat Simulator (бывший Lock On), Strike Suit Zero и т.д. (кто знает еще примеры - пишите в комментариях). В принципе любая игра, моделирующая реальные физические процессы, в том числе и просто кинематику с динамикой движения, может реализовывать те или иные САУ - этот подход проще, естественнее, а у разработчика уже есть есть набор готовых инструментов, предоставленных всякими Вышнеградскими, Ляпуновыми, Калманами, Чебышевами и прочими Коломогоровами, поэтому можно обойтись без изобретения велосипеда, т.к. его уже изобрели, да так, что получилась отдельная наука: Теория автоматического управления. Главное тут не переусердствовать. Одна тут только проблема: рассказывают про ТАУ не везде, не всем, зачастую мало и не очень понятно.

Немножко теории

Классическая система автоматического управления представленная на следующем рисунке:



Ключевым элементом любой САУ является регулятор представляющий из себя устройство, которое следит за состоянием объекта управления и обеспечивает требуемый закон управления. Процесс управления включает в себя: вычисление ошибки управления или сигнала рассогласования e (t ) как разницы между желаемой уставкой (set point или SP ) и текущей величиной процесса (process vale или PV ), после чего регулятор вырабатывает управляющие сигналы (manipulated value или MV ).


Одной из разновидностью регуляторов является пропорционально-интегрально-дифференцирующий (ПИД) регулятор , который формирует управляющий сигнал, являющийся суммой трёх слагаемых: пропорционального, интегрального и дифференциального.



Где, ошибка рассогласования, а также, - пропорциональная, - интегральная, - дифференциальная составляющие (термы) закона управления, который в итоговом виде описывается следующими формулами




Пропорциональная составляющая P - отвечает за т.н. пропорциональное управление, смысл которого в том, что выходной сигнал регулятора, противодействует отклонению регулируемой величины (ошибки рассогласования или еще это называют невязкой) от заданного значения. Чем больше ошибка рассогласования, тем больше командное отклонение регулятора. Это самый простой и очевидный закон управления. Недостаток пропорционального закона управления заключается в том, что регулятор никогда не стабилизируется в заданном значении, а увеличение коэффициента пропорциональности всегда приводит к автоколебаниям. Именно поэтому в довесок к пропорциональному закону управления приходиться использовать интегральный и дифференциальный.


Интегральная составляющая I накапливает (интегрирует) ошибку регулирования, что позволяет ПИД-регулятору устранять статическую ошибку (установившуюся ошибку, остаточное рассогласование). Или другими словами: интегральное звено всегда вносит некоторое смещение и если система подвержена некоторыми постоянным ошибкам, то оно их компенсирует (за счет своего смещения). А вот если же этих ошибок нет или они пренебрежительно малы, то эффект будет обратным - интегральная составляющая сама будет вносить ошибку смещения. Именно по этой причине её не используют, например, в задачах сверхточного позиционирования. Ключевым недостатком интегрального закона управления является эффект насыщения интегратора (Integrator windup).


Дифференциальная составляющая D пропорциональна темпу изменения отклонения регулируемой величины и предназначена для противодействия отклонениям от целевого значения, которые прогнозируются в будущем . Примечательно то, что дифференциальная компонента устраняет затухающие колебания. Дифференциальное регулирование особенно эффективно для процессов, которые имеют большие запаздывания. Недостатком дифференциального закона управления является его неустойчивость к воздействую шумов (Differentiation noise).


Таким образом, в зависимости от ситуации могут применятся П-, ПД-, ПИ- и ПИД-регуляторы, но основным законом управления в основном является пропорциональный (хотя в некоторых специфических задачах и могут использоваться исключительно только звенья дифференциаторов и интеграторов).


Казалось бы, вопрос реализации ПИД-регуляторов уже давно избит и здесь на Хабре есть парочка неплохих статей на эту тему в том числе и на Unity3D , также есть неплохая статья PID Without a PhD (перевод) и цикл статей в журнале "Современные технологии автоматизации" в двух частях: первая и вторая . Также к вашим услугам статья на Википедии (наиболее полную читайте в английском варианте). А на форумах коммьюнити Unity3D нет-нет, да и всплывет PID controller как и на gamedev.stackexchange


При вопрос по реализации ПИД-регуляторов несколько глубже чем и кажется. Настолько, что юных самоделкиных, решивших, реализовать такую схему регулирования ждет немало открытий чудных, а тема актуальная. Так что надеюсь сей опус, кому-нибудь да пригодиться, поэтому приступим.

Попытка номер раз

В качестве примера попытаемся реализовать схему регулирования на примере управления поворотом в простенькой космической 2D-аркаде, по шагам, начиная с самого начала (не забыли, что это туториал?).


Почему не 3D? Потому что реализация не измениться, за исключением того, что придется воротить ПИД-регулятор для контроля тангажа, рысканья и крена. Хотя вопрос корректного применения ПИД-регулирования вместе с кватернионами действительно интересный, возможно в будущем его и освящу, но даже в NASA предпочитают углы Эйлера вместо кватернионов, так что обойдемся простенькой моделью на двухмерной плоскости.


Для начала создадим сам объект игровой объект космического корабля, который будет состоять из собственно самого объекта корабля на верхнем уровне иерархии, прикрепим к нему дочерний объект Engine (чисто спецэффектов ради). Вот как это выглядит у меня:



А на сам объект космического корабля накидаем в инспекторе всяческих компонент. Забегая вперед, приведу скрин того, как он будет выглядеть в конце:



Но это потом, а пока в нем еще нет никаких скриптов, только стандартный джентльменский набор: Sprite Render, RigidBody2D, Polygon Collider, Audio Source (зачем?).


Собственно физика у нас сейчас самое главное и управление будет осуществляться исключительно через неё, в противном случае, применение ПИД-регулятора потеряло бы смысл. Масса нашего космического корабля оставим также в 1 кг, а все коэффициенты трения и гравитации равны нулю - в космосе же.


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


BaseBody.cs

using UnityEngine; using System.Collections; using System.Collections.Generic; namespace Assets.Scripts.SpaceShooter.Bodies { public class BaseBody: MonoBehaviour { readonly float _deafultTimeDelay = 0.05f; public static List _bodies = new List(); #region RigidBody public Rigidbody2D _rb2d; public Collider2D _c2d; #endregion #region References public Transform _myTransform; public GameObject _myObject; ///

/// Объект, который появляется при уничтожении /// public GameObject _explodePrefab; #endregion #region Audio public AudioSource _audioSource; /// /// Звуки, которые проигрываются при получении повреждения /// public AudioClip _hitSounds; /// /// Звуки, которые проигрываются при появлении объекта /// public AudioClip _awakeSounds; /// /// Звуки, которые воспроизводятся перед смертью /// public AudioClip _deadSounds; #endregion #region External Force Variables /// /// Внешние силы воздйствующие на объект /// public Vector2 _ExternalForces = new Vector2(); /// /// Текущий вектор скорости /// public Vector2 _V = new Vector2(); /// /// Текущий вектор силы гравитации /// public Vector2 _G = new Vector2(); #endregion public virtual void Awake() { Init(); } public virtual void Start() { } public virtual void Init() { _myTransform = this.transform; _myObject = gameObject; _rb2d = GetComponent(); _c2d = GetComponentsInChildren(); _audioSource = GetComponent(); PlayRandomSound(_awakeSounds); BaseBody bb = GetComponent(); _bodies.Add(bb); } /// /// Уничтожение персонажа /// public virtual void Destroy() { _bodies.Remove(this); for (int i = 0; i < _c2d.Length; i++) { _c2d[i].enabled = false; } float _t = PlayRandomSound(_deadSounds); StartCoroutine(WaitAndDestroy(_t)); } /// /// Ждем некоторое время перед уничтожением /// /// Время ожидания /// public IEnumerator WaitAndDestroy(float waitTime) { yield return new WaitForSeconds(waitTime); if (_explodePrefab) { Instantiate(_explodePrefab, transform.position, Quaternion.identity); } Destroy(gameObject, _deafultTimeDelay); } /// /// Проигрывание случайного звука /// /// Массив звуков /// Длительность проигрываемого звука public float PlayRandomSound(AudioClip audioClip) { float _t = 0; if (audioClip.Length > 0) { int _i = UnityEngine.Random.Range(0, audioClip.Length - 1); AudioClip _audioClip = audioClip[_i]; _t = _audioClip.length; _audioSource.PlayOneShot(_audioClip); } return _t; } /// /// Получение урона /// /// Уровень урона public virtual void Damage(float damage) { PlayRandomSound(_hitSounds); } } }


Вроде описали все что надо, даже больше чем нужно (в рамках этой статьи). Теперь отнаследуем от него класс корабля Ship , который должен уметь двигаться и поворачивать:


SpaceShip.cs

using UnityEngine; using System.Collections; using System.Collections.Generic; namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _rotation = 0f; public void FixedUpdate() { float torque = ControlRotate(_rotation); Vector2 force = ControlForce(_movement); _rb2d.AddTorque(torque); _rb2d.AddRelativeForce(force); } public float ControlRotate(Vector2 rotate) { float result = 0f; return result; } public Vector2 ControlForce(Vector2 movement) { Vector2 result = new Vector2(); return result; } } }


Пока в нем нет ничего интересно, на текущий момент это просто класс-заглушка.


Также опишем базовый(абстрактный) класс для всех контроллеров ввода BaseInputController:


BaseInputController.cs

using UnityEngine; using Assets.Scripts.SpaceShooter.Bodies; namespace Assets.Scripts.SpaceShooter.InputController { public enum eSpriteRotation { Rigth = 0, Up = -90, Left = -180, Down = -270 } public abstract class BaseInputController: MonoBehaviour { public GameObject _agentObject; public Ship _agentBody; // Ссылка на компонент логики корабля public eSpriteRotation _spriteOrientation = eSpriteRotation.Up; //Это связано с нестандартной // ориентации спрайта "вверх" вместо "вправо" public abstract void ControlRotate(float dt); public abstract void ControlForce(float dt); public virtual void Start() { _agentObject = gameObject; _agentBody = gameObject.GetComponent(); } public virtual void FixedUpdate() { float dt = Time.fixedDeltaTime; ControlRotate(dt); ControlForce(dt); } public virtual void Update() { //TO DO } } }


И наконец, класс контроллера игрока PlayerFigtherInput :


PlayerInput.cs

using UnityEngine; using Assets.Scripts.SpaceShooter.Bodies; namespace Assets.Scripts.SpaceShooter.InputController { public class PlayerFigtherInput: BaseInputController { public override void ControlRotate(float dt) { // Определяем позицию мыши относительно игрока Vector3 worldPos = Input.mousePosition; worldPos = Camera.main.ScreenToWorldPoint(worldPos); // Сохраняем координаты указателя мыши float dx = -this.transform.position.x + worldPos.x; float dy = -this.transform.position.y + worldPos.y; //Передаем направление Vector2 target = new Vector2(dx, dy); _agentBody._target = target; //Вычисляем поворот в соответствии с нажатием клавиш float targetAngle = Mathf.Atan2(dy, dx) * Mathf.Rad2Deg; _agentBody._targetAngle = targetAngle + (float)_spriteOrientation; } public override void ControlForce(float dt) { //Передаем movement _agentBody._movement = Input.GetAxis("Vertical") * Vector2.up + Input.GetAxis("Horizontal") * Vector2.right; } } }


Вроде бы закончили, теперь наконец можно перейти к тому, ради чего все это затевалось, т.е. ПИД-регуляторам (не забыли надеюсь?). Его реализация кажется простой до безобразия:


using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Assets.Scripts.Regulator { // Этот атрибут необходим для того что бы поля регулятора // отображались в инспекторе и сериализовывались public class SimplePID { public float Kp, Ki, Kd; private float lastError; private float P, I, D; public SimplePID() { Kp = 1f; Ki = 0; Kd = 0.2f; } public SimplePID(float pFactor, float iFactor, float dFactor) { this.Kp = pFactor; this.Ki = iFactor; this.Kd = dFactor; } public float Update(float error, float dt) { P = error; I += error * dt; D = (error - lastError) / dt; lastError = error; float CO = P * Kp + I * Ki + D * Kd; return CO; } } }

Значения коэффициентов по умолчанию возьмем с потолка: это будет тривиальный единичный коэффициент пропорционального закона управления Kp = 1, небольшое значение коэффициента для дифференциального закона управления Kd = 0.2, который должен устранить ожидаемые колебания и нулевое значение для Ki, которое выбрано потому, что в нашей программной модели нет никаких статичных ошибок (но вы всегда можете их внести, а потом героически побороться с помощью интегратора).


Теперь вернемся к нашему классу SpaceShip и попробуем заюзать наше творение в качестве регулятора поворота космического корабля в методе ControlRotate:


public float ControlRotate(Vector2 rotate) { float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение MV = _angleController.Update(angleError, dt); return MV; }

ПИД-регулятор будет осуществлять точное угловое позиционировая космического корабля только за счет крутящего момента . Все честно, физика и САУ, почти как в реальной жизни.


И без этих ваших Quaternion.Lerp

if (!_rb2d.freezeRotation) rb2d.freezeRotation = true; float deltaAngle = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); float T = dt * Mathf.Abs(_rotationSpeed / deltaAngle); // Трансформируем угол в вектор Quaternion rot = Quaternion.Lerp(_myTransform.rotation, Quaternion.Euler(new Vector3(0, 0, targetAngle)), T); // Изменяем поворот объекта _myTransform.rotation = rot;


Получившейся исходный код Ship.cs под спойлером

using UnityEngine; using Assets.Scripts.Regulator; namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public GameObject _flame; public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _targetAngle = 0f; public float _angle = 0f; public SimplePID _angleController = new SimplePID(); public void FixedUpdate() { float torque = ControlRotate(_targetAngle); Vector2 force = ControlForce(_movement); _rb2d.AddTorque(torque); _rb2d.AddRelativeForce(force); } public float ControlRotate(float rotate) { float MV = 0f; float dt = Time.fixedDeltaTime; _angle = _myTransform.eulerAngles.z; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_angle, rotate); //Получаем корректирующее ускорение MV = _angleController.Update(angleError, dt); return MV; } public Vector2 ControlForce(Vector2 movement) { Vector2 MV = new Vector2(); //Кусок кода спецэффекта работающего двигателя ради if (movement != Vector2.zero) { if (_flame != null) { _flame.SetActive(true); } } else { if (_flame != null) { _flame.SetActive(false); } } MV = movement; return MV; } } }


Все? Расходимся по домам?



WTF! Что происходит? Почему корабль поворачивается как-то странно? И почему он так резко отскакивает от других объектов? Неужели этот глупый ПИД-регулятор не работает?


Без паники! Давайте попробуем разобраться что происходит.


В момент получения нового значения SP, происходит резкий (ступенчатый) скачок рассогласования ошибки, которая, как мы помним, вычисляется вот так: соответственно происходит резкий скачок производной ошибки , которую мы вычисляем в этой строчке кода:


D = (error - lastError) / dt;

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


Думаю что настал момент построить графики переходного процесса : ступенчатое воздействие от S(t) = 0 в SP(t) = 90 градусов для тела массой в 1 кг, длинной плеча силы в 1 метр и шагом сетки дифференцирования 0.02 с - прям как в нашем примере на Unity3D (на самом деле не совсем, при построении этих графиков не учитывалось, что момент инерции зависит от геометрии твердого тела, поэтому переходный процесс будет немножко другой, но все же достаточно похожий для демонстрации). Все величены на грифике приведены в абсолютных значениях:


Хм, что здесь происходит? Куда улетел отклик ПИД-регулятора?


Поздравляю, мы только что столкнулись с таким явлением как "удар" (kick). Очевидно, что в момент времени, когда процесс еще PV = 0, а уставка уже SP = 90, то при численном дифференцировании получим значение производной порядка 4500, которое умножится на Kd=0.2 и сложится с пропорциональным теромом, так что на выходе мы получим значение углового ускорения 990, а это уже форменное надругательство над физической моделью Unity3D (угловые скорости будут достигать 18000 град/с… я думаю это предельное значение угловой скорости для RigidBody2D).


  • Может стоит подобрать коэффициенты ручками, так чтобы скачок был не таким сильным?
  • Нет! Самое лучше чего мы таким образом сможем добиться - небольшая амплитуда скачка производной, однако сам скачок как был так и останется, при этом можно докрутиться до полной неэффективности дифференциальной составляющей.

Впрочем можете поэкспериментировать.

Попытка номер два. Сатурация

Логично, что привод (в нашем случае виртуальные маневровые двигатели SpaceShip), не может отрабатывать сколько угодно большие значения которые может выдать наш безумный регулятор. Так что первое что мы сделаем - сатурируем выход регулятора:


public float ControlRotate(Vector2 rotate, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение CO = _angleController.Update(angleError, dt); //Сатурируем MV = CO; if (MV > thrust) MV = thrust; if (MV< -thrust) MV = -thrust; return MV; }

А очередной раз переписанный класс Ship полностью выглядит так

namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public GameObject _flame; public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _targetAngle = 0f; public float _angle = 0f; public float _thrust = 1f; public SimplePID _angleController = new SimplePID(0.1f,0f,0.05f); public void FixedUpdate() { _torque = ControlRotate(_targetAngle, _thrust); _force = ControlForce(_movement); _rb2d.AddTorque(_torque); _rb2d.AddRelativeForce(_force); } public float ControlRotate(float targetAngle, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение CO = _angleController.Update(angleError, dt); //Сатурируем MV = CO; if (MV > thrust) MV = thrust; if (MV< -thrust) MV = -thrust; return MV; } public Vector2 ControlForce(Vector2 movement) { Vector2 MV = new Vector2(); if (movement != Vector2.zero) { if (_flame != null) { _flame.SetActive(true); } } else { if (_flame != null) { _flame.SetActive(false); } } MV = movement * _thrust; return MV; } public void Update() { } } }


Итоговая схема нашего САУ тогда станет уже вот такой


При этом уже становится понятно, что выход контроллера CO(t) немного не одно и тоже, что управляемая величина процесса MV(t) .


Собственно с этого места можно уже добавлять новую игровую сущность - привод , через которую и будет осуществляться управление процессом, логика работы которой может быть более сложной, чем просто Mathf.Clamp(), например, можно ввести дискретизацию значений (дабы не перегружать игровую физику величинами идущими шестыми после запятой), мертвую зону (опять таки не имеет смысл перегружать физику сверхмалыми реакциями), ввести задержку в упраление и нелинейность (например, сигмоиду) привода, после чего посмотреть, что из этого получится.


Запустив игру, мы обнаружим, что космический корабль стал наконец управляемым:



Если построить графики, то можно увидеть, что реакция контроллера стала уже вот такой:


Здесь уже используются нормированные величены, углы поделены на значение SP, а выход контроллера отнормирован относительно максимального значения на котором уже происходит сатурация.

Ниже приведена известна таблица влияния увеличения параметров ПИД-регулятора (как уменьшить шрифт, а то таблица безе переносов не лезет? ):



А общий алгоритм ручной настройки ПИД-регулятора следующий:


  1. Подбираем пропорциональный коэффициенты при отключенных дифференциальных и интегральных звеньях до тех пор пока не начнутся автоколебания.
  2. Постепенно увеличивая дифференциальную составляющую избавляемся от автоколебаний
  3. Если наблюдается остаточная ошибка регулирования (смещение), то устраняем её за счет интегральной составляющей.

Каких-то общих значений параметров ПИД-регулятора нет: конкретные значения зависят исключительно от параметров процесса (его передаточной характеристики): ПИД-регулятор отлично работающий с одним объектом управления окажется неработоспособным с другим. Более того, коэффициенты при пропорциональной, интегральной и дифференциальной составляющих еще и взаимозависимы.


Попытка номер три. Еще раз производные

Приделав костыль в виде ограничения значений выхода контроллера мы так и не решили самую главную проблему нашего регулятора - дифференциальная составляющая плохо себя чувствует при ступенчатом изменении ошибки на входе регуляторе. На самом деле есть множество других костылей, например, в момент скачкообразного изменения SP "отключать" дифференциальную составляющую или же поставить фильтры нижних частот между SP(t) и операцией за счет которого будет происходить плавное нарастание ошибки, а можно совсем развернуться и впендюрить самый настоящий фильтр Калмана для сглаживания входных данных. В общем костылей много, и добавить наблюдателя конечно хотелось бы, но не в этот раз.


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



Ничего не заметили? Если хорошенько присмотреться, то можно обнаружить, что вообще-то SP(t), не меняется во времени (за исключением моментов ступенчатого изменения, когда регулятор получает новую команду), т.е. её производная равна нулю:





Иными словами, вместо производной ошибки, которая дифференцируема не везде мы можем использовать производную от процесса, который в мире классической механики как правило непрерывен и дифференцируем везде, а схема нашей САУ уже приобретет следующий вид:




Модифицируем код регулятора:


using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Assets.Scripts.Regulator { public class SimplePID { public float Kp, Ki, Kd; private float P, I, D; private float lastPV = 0f; public SimplePID() { Kp = 1f; Ki = 0f; Kd = 0.2f; } public SimplePID(float pFactor, float iFactor, float dFactor) { this.Kp = pFactor; this.Ki = iFactor; this.Kd = dFactor; } public float Update(float error, float PV, float dt) { P = error; I += error * dt; D = -(PV - lastPV) / dt; lastPV = PV; float CO = Kp * P + Ki * I + Kd * D; return CO; } } }

И немного изменим метод ControlRotate:


public float ControlRotate(Vector2 rotate, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; //Вычисляем ошибку float angleError = Mathf.DeltaAngle(_myTransform.eulerAngles.z, targetAngle); //Получаем корректирующее ускорение CO = _angleController.Update(angleError, _myTransform.eulerAngles.z, dt); //Сатурируем MV = CO; if (CO > < -thrust) MV = -thrust; return MV; }

И-и-и-и… если запустить игру, то обнаружиться, что на самом деле ничего ничего не изменилось с последней попытки, что и требовалось доказать. Однако, если убрать сатурацию, то график реакции регулятора будет выглядеть вот так:


Скачок CO(t) по прежнему присутствует, однако он уже не такой большой как был в самом начале, а самое главное - он стал предсказуемым, т.к. обеспечивается исключительно пропорциональной составляющей, и ограничен максимально возможной ошибкой рассогласования и пропорциональным коэффициентом ПИД-регулятора (а это уже намекает на то, что Kp имеет смысл выбрать все же меньше единицы, например, 1/90f), но не зависит от шага сетки дифференцирования (т.е. dt ). В общем, я настоятельно рекомендую использовать именно производную процесса, а не ошибки.


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

Попытка номер четыре. Альтернативные реализации ПИД-регулятор

Помимо описанного выше идеального представления ПИД-регулятора, на практике часто применяется стандартная форма, без коэффициентов Ki и Kd , вместо которых используются временные постоянные.


Такой подход связан с тем, что ряд методик настройки ПИД-регулятора основан на частотных характеристиках ПИД-регулятора и процесса. Собственно вся ТАУ и крутится вокруг частотных характеристик процессов, поэтому для желающих углубиться, и, внезапно, столкнувшихся с альтернативной номенклатурой, приведу пример т.н. стандартной формы ПИД-регулятора:




где, - постоянная дифференцирования, влияющая на прогнозирование состояния системы регулятором,
- постоянная интегрирования, влияющая на интервал усреднения ошибки интегральным звеном.


Основные принципы настройки ПИД-регулятора в стандартной форме аналогичны идеализированному ПИД-регулятору:

  • увеличение пропорционального коэффициента увеличивает быстродействие и снижает запас устойчивости;
  • с уменьшением интегральной составляющей ошибка регулирования с течением времени уменьшается быстрее;
  • уменьшение постоянной интегрирования уменьшает запас устойчивости;
  • увеличение дифференциальной составляющей увеличивает запас устойчивости и быстродействие

Исходный код стандартной формы, вы можете найти под спойлером

namespace Assets.Scripts.Regulator { public class StandartPID { public float Kp, Ti, Td; public float error, CO; public float P, I, D; private float lastPV = 0f; public StandartPID() { Kp = 0.1f; Ti = 10000f; Td = 0.5f; bias = 0f; } public StandartPID(float Kp, float Ti, float Td) { this.Kp = Kp; this.Ti = Ti; this.Td = Td; } public float Update(float error, float PV, float dt) { this.error = error; P = error; I += (1 / Ti) * error * dt; D = -Td * (PV - lastPV) / dt; CO = Kp * (P + I + D); lastPV = PV; return CO; } } }

В качестве значений по умолчанию, выбраны Kp = 0.01, Ti = 10000, Td = 0.5 - при таких значениях корабль поворачивается достаточно быстро и обладает некоторым запасом устойчивости.


Помимо такой формы ПИД-регулятора, часто используется т.н. реккурентная форма :



Не будем на ней останавливаться, т.к. она актуальна прежде всего для хардверных программистов, работающих с FPGA и микроконтроллерами, где такая реализация значительно удобнее и эффективнее. В нашем же случае - давайте что-нибудь сваям на Unity3D - это просто еще одна реализация ПИД-контроллера, которая ни чем не лучше других и даже менее понятная, так что еще раз дружно порадуемся как хорошо программировать в уютненьком C#, а не в жутком и страшном VHDL, например.

Вместо заключения. Куда бы еще присобачить ПИД-регулятор

Теперь попробуем немного усложнить управление корабля используя двухконтурное управление: один ПИД-регулятор, уже знакомый нам _angleController, отвечает по прежнему за угловое позиционирование, а вот второй - новый, _angularVelocityController - контролирует скорость поворота:


public float ControlRotate(float targetAngle, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; _angle = _myTransform.eulerAngles.z; //Контроллер угла поворота float angleError = Mathf.DeltaAngle(_angle, targetAngle); float torqueCorrectionForAngle = _angleController.Update(angleError, _angle, dt); //Контроллер стабилизации скорости float angularVelocityError = -_rb2d.angularVelocity; float torqueCorrectionForAngularVelocity = _angularVelocityController.Update(angularVelocityError, -angularVelocityError, dt); //Суммарный выход контроллера CO = torqueCorrectionForAngle + torqueCorrectionForAngularVelocity; //Дискретизируем с шагом 100 CO = Mathf.Round(100f * CO) / 100f; //Сатурируем MV = CO; if (CO > thrust) MV = thrust; if (CO < -thrust) MV = -thrust; return MV; }

Назначение второго регулятора - гашение избыточных угловых скоростей, за счет изменения крутящего момента - это сродни наличию углового трения, которое мы отключили еще при создании игрового объекта. Такая схема управления [возможно] позволит получить более стабильное поведение корабля, и даже обойтись только пропорциональными коэффициентами управления - второй регулятор будет гасить все колебания, выполняя функцию, аналогичную дифференциальной составляющей первого регулятора.


Помимо этого, добавим новый класс ввода игрока - PlayerInputCorvette, в котором повороты буду осуществляться уже за счет нажатия клавиш "вправо-влево", а целеуказание с помощью мыши мы оставим для чего-нибудь более полезного, например, для управления турелью. Заодно у нас теперь появился такой параметр как _turnRate - отвечающий за скорость/отзывчивость поворота (не понятно только куда его поместить лучше в InputCOntroller или все же Ship).


public class PlayerCorvetteInput: BaseInputController { public float _turnSpeed = 90f; public override void ControlRotate() { // Находим указатель мыши Vector3 worldPos = Input.mousePosition; worldPos = Camera.main.ScreenToWorldPoint(worldPos); // Сохраняем относительные координаты указателя мыши float dx = -this.transform.position.x + worldPos.x; float dy = -this.transform.position.y + worldPos.y; //Передаем направление указателя мыши Vector2 target = new Vector2(dx, dy); _agentBody._target = target; //Вычисляем поворот в соответствии с нажатием клавиш _agentBody._rotation -= Input.GetAxis("Horizontal") * _turnSpeed * Time.deltaTime; } public override void ControlForce() { //Передаем movement _agentBody._movement = Input.GetAxis("Vertical") * Vector2.up; } }

Также для наглядности накидаем на коленках скрипт для отображения отладочной информации

namespace Assets.Scripts.SpaceShooter.UI { public class Debugger: MonoBehaviour { Ship _ship; BaseInputController _controller; List _pids = new List(); List _names = new List(); Vector2 _orientation = new Vector2(); // Use this for initialization void Start() { _ship = GetComponent(); _controller = GetComponent(); _pids.Add(_ship._angleController); _names.Add("Angle controller"); _pids.Add(_ship._angularVelocityController); _names.Add("Angular velocity controller"); } // Update is called once per frame void Update() { DrawDebug(); } Vector3 GetDiretion(eSpriteRotation spriteRotation) { switch (_controller._spriteOrientation) { case eSpriteRotation.Rigth: return transform.right; case eSpriteRotation.Up: return transform.up; case eSpriteRotation.Left: return -transform.right; case eSpriteRotation.Down: return -transform.up; } return Vector3.zero; } void DrawDebug() { //Направление поворота Vector3 vectorToTarget = transform.position + 5f * new Vector3(-Mathf.Sin(_ship._targetAngle * Mathf.Deg2Rad), Mathf.Cos(_ship._targetAngle * Mathf.Deg2Rad), 0f); // Текущее направление Vector3 heading = transform.position + 4f * GetDiretion(_controller._spriteOrientation); //Угловое ускорение Vector3 torque = heading - transform.right * _ship._Torque; Debug.DrawLine(transform.position, vectorToTarget, Color.white); Debug.DrawLine(transform.position, heading, Color.green); Debug.DrawLine(heading, torque, Color.red); } void OnGUI() { float x0 = 10; float y0 = 100; float dx = 200; float dy = 40; float SliderKpMax = 1; float SliderKpMin = 0; float SliderKiMax = .5f; float SliderKiMin = -.5f; float SliderKdMax = .5f; float SliderKdMin = 0; int i = 0; foreach (SimplePID pid in _pids) { y0 += 2 * dy; GUI.Box(new Rect(25 + x0, 5 + y0, dx, dy), ""); pid.Kp = GUI.HorizontalSlider(new Rect(25 + x0, 5 + y0, 200, 10), pid.Kp, SliderKpMin, SliderKpMax); pid.Ki = GUI.HorizontalSlider(new Rect(25 + x0, 20 + y0, 200, 10), pid.Ki, SliderKiMin, SliderKiMax); pid.Kd = GUI.HorizontalSlider(new Rect(25 + x0, 35 + y0, 200, 10), pid.Kd, SliderKdMin, SliderKdMax); GUIStyle style1 = new GUIStyle(); style1.alignment = TextAnchor.MiddleRight; style1.fontStyle = FontStyle.Bold; style1.normal.textColor = Color.yellow; style1.fontSize = 9; GUI.Label(new Rect(0 + x0, 5 + y0, 20, 10), "Kp", style1); GUI.Label(new Rect(0 + x0, 20 + y0, 20, 10), "Ki", style1); GUI.Label(new Rect(0 + x0, 35 + y0, 20, 10), "Kd", style1); GUIStyle style2 = new GUIStyle(); style2.alignment = TextAnchor.MiddleLeft; style2.fontStyle = FontStyle.Bold; style2.normal.textColor = Color.yellow; style2.fontSize = 9; GUI.TextField(new Rect(235 + x0, 5 + y0, 60, 10), pid.Kp.ToString(), style2); GUI.TextField(new Rect(235 + x0, 20 + y0, 60, 10), pid.Ki.ToString(), style2); GUI.TextField(new Rect(235 + x0, 35 + y0, 60, 10), pid.Kd.ToString(), style2); GUI.Label(new Rect(0 + x0, -8 + y0, 200, 10), _names, style2); } } } }


Класс Ship также претерпел необратимые мутации и теперь должен выглядеть вот так:

namespace Assets.Scripts.SpaceShooter.Bodies { public class Ship: BaseBody { public GameObject _flame; public Vector2 _movement = new Vector2(); public Vector2 _target = new Vector2(); public float _targetAngle = 0f; public float _angle = 0f; public float _thrust = 1f; public SimplePID _angleController = new SimplePID(0.1f,0f,0.05f); public SimplePID _angularVelocityController = new SimplePID(0f,0f,0f); private float _torque = 0f; public float _Torque { get { return _torque; } } private Vector2 _force = new Vector2(); public Vector2 _Force { get { return _force; } } public void FixedUpdate() { _torque = ControlRotate(_targetAngle, _thrust); _force = ControlForce(_movement, _thrust); _rb2d.AddTorque(_torque); _rb2d.AddRelativeForce(_force); } public float ControlRotate(float targetAngle, float thrust) { float CO = 0f; float MV = 0f; float dt = Time.fixedDeltaTime; _angle = _myTransform.eulerAngles.z; //Контроллер угла поворота float angleError = Mathf.DeltaAngle(_angle, targetAngle); float torqueCorrectionForAngle = _angleController.Update(angleError, _angle, dt); //Контроллер стабилизации скорости float angularVelocityError = -_rb2d.angularVelocity; float torqueCorrectionForAngularVelocity = _angularVelocityController.Update(angularVelocityError, -angularVelocityError, dt); //Суммарный выход контроллера CO = torqueCorrectionForAngle + torqueCorrectionForAngularVelocity; //Дискретизируем с шагом 100 CO = Mathf.Round(100f * CO) / 100f; //Сатурируем MV = CO; if (CO > thrust) MV = thrust; if (CO < -thrust) MV = -thrust; return MV; } public Vector2 ControlForce(Vector2 movement, float thrust) { Vector2 MV = new Vector2(); if (movement != Vector2.zero) { if (_flame != null) { _flame.SetActive(true); } } else { if (_flame != null) { _flame.SetActive(false); } } MV = movement * thrust; return MV; } public void Update() { } } }

Еще немного ссылок на другие примеры