Аэродинамика в Unity ч.2

В первой части я рассказал о том, как с помощью Unity заставил самолёт взлететь. Сейчас речь пойдёт о том, как я приделал к нему штурвал. И на какие ухищрения пришлось пойти в реализации аркадной физики моего летательного аппарата.

Маневры

Для управления самолётом в воздухе есть три основных манёвра.

Крен (Roll) — Поворот самолёта вокруг своей продольной оси

Рысканье (Yaw) — Поворот самолета вокруг своей вертикальной оси

Тангаж (Pitch) — Поворот самолёта вокруг своей поперечной оси

Аэродинамика в Unity ч.2

Реализация

Немного о структуре прототипа.

Аэродинамика в Unity ч.2

AircraftController — MonoBehaviour, который принимает ввод игрока и в обработчике Update передаёт эти данные в объект currentAircraft.

private void Update() { if (currentAircraft != null) { currentAircraft.UpdateControlState(throttleInput, yawInput, pitchRollInput); } }

В каждом FixedUpdate рассчитывается физика самолёта. Вообще - я реализовал это в отдельном MonoBehaviour, но в контексте поста это неважно.

private void FixedUpdate() { aircraft.physics.UpdatePhysics(aircraft.controlState, aircraft.data); }

AircraftModel — класс, реализующий сущность самолёта. Содержит в себе несколько полей, каждое из которых отвечает за свою область. А также собирает и спавнит самолёт по префабу в своём конструкторе.

AircraftData — наследует у ScriptableObject. Я его использую для хранения всех характеристик самолёта. Мощность двигателя, площадь крыла, масса и так далее.

AircraftControlState — Содержит данные о текущем состоянии рулей управления а также текущую тягу двигателя. Не путать с AircraftController, который получает от игрока нажатые кнопки.

AircraftPhysics — Самый интересный для нас класс. Методы приведенные в первой части как раз отсюда. В конце поста я размещу класс целиком. А до этого тезисно опишу, какие он претерпел изменения после предыдущего поста, и что нового я туда добавил.

Что изменилось?

Во-первых, я добавил в класс AircraftPhysics пять полей класса Transform. Они предназначены для хранения определённых точек самолёта. Нос, хвост, два крыла и центр масс. К этим точкам мы будем применять силы.

ThrustForce_calculate — сила тяги теперь воздействует на точку носа самолёта. Более реалистично, т.к. сила прикладывается именно пропеллером.

DragForce_calculate — сила сопротивления теперь зависит от угла отклонения. Чем больше самолёт повернут от потока воздуха, тем больше площадь сопротивления. Регулируется minDrag и maxDrag из aircraftData.

LiftForce_calculate — Подъёмная сила теперь применяется на три точки: два крыла и хвостовой стабилизатор. Это помогло мне стабилизировать взлёт и посадку. Также в расчете подъёмной силы теперь участвует угол атаки крыла, и предусмотрен срыв потока (когда угол атаки становится слишком большим подъемная сила резко падает).

Угол атаки — угол между хордой крыла и направлением воздушного потока
Угол атаки — угол между хордой крыла и направлением воздушного потока

Что добавлено?

Управление

PitchForce_calculate — передаётся сила для тангажа, действует на хвост самолёта. Считается по аналогии с подъёмной силой.

YawForce_calculate — передаётся сила для рысканья. Изначально прилагал эту силу на хвост, т. к. за этот манёвр вроде как отвечает поворот вертикального стабилизатора, но у меня не получилось это сделать нормально, и самолёт как будто носило боком. Так что теперь сила действует на центр масс, направлена в сторону поворота под прямым углом.

RollForce_calculate — передаётся сила для крена самолета. Прилагается сразу на оба крыла. На одно вниз, на второе — вверх. Сюда добавил сопротивление, которое зависит от угловой скорости самолёта вокруг своей оси. Без него в бочке самолёт неоправданно быстро закручивался.

Стабилизация

stabPitchForce_calculate, stabYawForce_calculate, stabRollForce_calculate — Все три метода сообщают самолёту вращающий момент, который зависит от угловой скорости в каждой из трех осей отдельно.

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

Если игрок жмёт клавишу крена, он ждёт, что крен прекратится после того, как он отпустит кнопку. Для этого тут эти методы.

StabilizeForce_calculate — это, наверное, самое большое моё упрощение. Мне нужно было добиться того, чтобы самолёт выравнивался по вектору скорости. Я пытался выдумать аналоги подъемной силы, которые действуют на вертикальный и горизонтальный стабилизаторы в зависимости от отклонения, но настроить параметры у меня не получилось.

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

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

В предыдущих трех видео тоже можно наблюдать эту силу, я обозначил её оранжевым цветом.

Таким образом я реализовал управление самолётом. Понятно, что нужно будет доводить до ума, но для прототипа достаточно.

Спасибо за внимание!

В своём телеграм-канале я регулярно делюсь прогрессом

Также, если кому-то интересно, привожу класс AircraftPhysics полностью

using System.Runtime.InteropServices; using TMPro; using UnityEngine; using UnityEngine.UI; using UnityEngine.UIElements; [RequireComponent(typeof(Rigidbody))] public class AircraftPhysics { //Ñîïðîòèâëåíèå âîçäóõà //to do: âîçìîæíî, ïîòîì áðàòü îòêóäà-òî. Ê ïðèìåðó, ñâåðõó âîçäóõ ðàçðåæåííûé - îãðàíè÷èâàåì ïîòîëîê private float airDensity = 1.225f; public Rigidbody rigidBody; public Transform transform; public Vector3 velocity; public Transform centerOfMass; public Transform tailPoint; public Transform nosePoint; public Transform leftWing; public Transform rightWing; private float aircraftAttackAngle; private float flowAngle; private Vector3 thrustForce; private Vector3 dragForce; private Vector3 liftForce; private Vector3 liftForceStab; private Vector3 pitchForce; private Vector3 yawForce; private Vector3 rollForce; private Vector3 mainStabilizationForce; public WheelCollider NoseWheelColliderR; public WheelCollider NoseWheelColliderL; public WheelCollider tailWheelCollider; private float currentBrake; public AircraftPhysics(GameObject gameObject, AircraftData data) { transform = gameObject.transform; Transform points = gameObject.transform.Find("Points"); centerOfMass = points.Find("CenterOfMass"); tailPoint = points.Find("Tail"); nosePoint = points.Find("Nose"); leftWing = points.Find("LeftWing"); rightWing = points.Find("RightWing"); rigidBody = gameObject.GetComponent<Rigidbody>(); rigidBody.mass = data.mass; NoseWheelColliderR = gameObject.transform.Find("NoseWheelColliderR").GetComponent<WheelCollider>(); NoseWheelColliderL = gameObject.transform.Find("NoseWheelColliderL").GetComponent<WheelCollider>(); tailWheelCollider = gameObject.transform.Find("TailWheelCollider").GetComponent<WheelCollider>(); currentBrake = 0; } public void UpdatePhysics(AircraftControlState controlState, AircraftData data) { velocity = rigidBody.linearVelocity; calculateAngles(); ThrustForce_calculate(controlState, data); ChassisControl(controlState, data); if (velocity.sqrMagnitude > 0.01) { DragForce_calculate(controlState, data); LiftForce_calculate(controlState, data); StabilizeForce_calculate(controlState, data); PitchForce_calculate(controlState, data); YawForce_calculate(controlState, data); RollForce_calculate(controlState, data); StabPitchForce_calculate(controlState, data); StabYawForce_calculate(controlState, data); StabRollForce_calculate(controlState, data); } } private void ThrustForce_calculate(AircraftControlState controlState, AircraftData data) { thrustForce = transform.forward * controlState.currentThrust; rigidBody.AddForceAtPosition(thrustForce, nosePoint.position); } private void DragForce_calculate(AircraftControlState state, AircraftData data) { Vector3 dragForceDir = velocity.normalized * -1; float dragArea = data.minDrag + (data.maxDrag - data.minDrag) * (flowAngle / 180f); dragForce = dragForceDir * airDragMagnitude(velocity.magnitude, dragArea); rigidBody.AddForceAtPosition(dragForce, centerOfMass.position); } private void LiftForce_calculate(AircraftControlState state, AircraftData data) { float CL = calculateCL(data.wingsCurveSlope, data.wingAttackAngle + aircraftAttackAngle, data.stallAngle); Vector3 liftForceDir = Vector3.Cross(velocity, transform.right).normalized; liftForce = liftForceDir * airDragMagnitude(velocity.magnitude, data.wingArea) * CL; rigidBody.AddForceAtPosition(liftForce, leftWing.position); rigidBody.AddForceAtPosition(liftForce, rightWing.position); liftForceStab = liftForceDir * airDragMagnitude(velocity.magnitude, data.stabArea) * CL; rigidBody.AddForceAtPosition(liftForceStab, tailPoint.position); } private void StabilizeForce_calculate(AircraftControlState state, AircraftData data) { Vector3 stabForceDir = velocity.normalized; float stabCL = calculateCL(data.stabilizationCurveSlope, flowAngle, data.stabilizationStallAngle); mainStabilizationForce = stabForceDir * airDragMagnitude(velocity.magnitude, data.stabilizationCoef ) * stabCL; rigidBody.AddForceAtPosition(-1 * mainStabilizationForce, centerOfMass.position); rigidBody.AddForceAtPosition(mainStabilizationForce, nosePoint.position); } private void PitchForce_calculate(AircraftControlState state, AircraftData data) { float CL = calculateCL(data.wingsCurveSlope, data.wingAttackAngle, data.stallAngle); Vector3 pitchForceDir = -1 * transform.up; pitchForce = pitchForceDir * airDragMagnitude(velocity.magnitude, state.currentPitch) * CL; rigidBody.AddForceAtPosition(pitchForce, tailPoint.position); } private void YawForce_calculate(AircraftControlState state, AircraftData data) { Vector3 yawForceDir = transform.right; yawForce = yawForceDir * airDragMagnitude(velocity.magnitude, state.currentYaw); rigidBody.AddForceAtPosition(yawForce, centerOfMass.position); } private void RollForce_calculate(AircraftControlState state, AircraftData data) { Vector3 rollForceDir = transform.up; if (state.currentRoll < 0) rollForceDir *= -1; float rollSpeed = transform.InverseTransformDirection(rigidBody.angularVelocity).z; float rollDragM = airDragMagnitude(rollSpeed, data.wingArea * 7f); float rollForceM = airDragMagnitude(velocity.magnitude, Mathf.Abs(state.currentRoll)); rollForce = rollForceDir * (rollForceM - rollDragM); rigidBody.AddForceAtPosition(rollForce, leftWing.position); rigidBody.AddForceAtPosition(-1 * rollForce, rightWing.position); } private void StabPitchForce_calculate(AircraftControlState state, AircraftData data) { float pitchSpeed = transform.InverseTransformDirection(rigidBody.angularVelocity).x; float pitchStabilizationTorque = -pitchSpeed * data.pitchStabCoef; rigidBody.AddRelativeTorque(pitchStabilizationTorque, 0f, 0f, ForceMode.Acceleration); } private void StabYawForce_calculate(AircraftControlState state, AircraftData data) { float yawSpeed = transform.InverseTransformDirection(rigidBody.angularVelocity).y; float yawStabilizationTorque = -yawSpeed * data.yawStabCoef; rigidBody.AddRelativeTorque(0f, yawStabilizationTorque, 0f, ForceMode.Acceleration); } private void StabRollForce_calculate(AircraftControlState state, AircraftData data) { float rollSpeed = transform.InverseTransformDirection(rigidBody.angularVelocity).z; float rollStabilizationTorque = -rollSpeed * data.rollStabCoef; rigidBody.AddRelativeTorque(0f, 0f, rollStabilizationTorque, ForceMode.Acceleration); } private void calculateAngles() { aircraftAttackAngle = Vector3.SignedAngle( Vector3.ProjectOnPlane(velocity, transform.right), transform.forward, transform.right * -1); if (Mathf.Abs(aircraftAttackAngle) > 180f) aircraftAttackAngle = -1 * (360 - aircraftAttackAngle); flowAngle = Vector3.Angle(velocity, transform.forward); } private float calculateCL(float liftCurveSlope, float alphaAngle, float stallAngle) { float CL0 = 0.02f; float alphaRad = alphaAngle * Mathf.Deg2Rad; float stallRad = stallAngle * Mathf.Deg2Rad; float CL_linear = CL0 + liftCurveSlope * alphaRad; float CL_stall = CL0 + liftCurveSlope * stallRad; if (alphaAngle <= stallAngle) return CL_linear; else { float dropRate = 0.98f; // to do: Âûíåñòè â aircraftData? float alphaMax = stallAngle + 1.0f; // to do: Âûíåñòè â aircraftData? float CL_min = CL_stall * (1.0f - dropRate); float t = Mathf.Clamp((alphaAngle - stallAngle) / (alphaMax - stallAngle), 0.0f, 1.0f); return CL_stall + (CL_min - CL_stall) * t; } } private float airDragMagnitude(float velocity, float dragArea) { float result = 0.5f * airDensity * velocity * velocity * dragArea; return result; } private void ChassisControl(AircraftControlState controlState, AircraftData data) { bool hasThrust = (Mathf.Abs(controlState.currentThrust) > 0.1f || velocity.magnitude > 1); float targetBrake = hasThrust ? 0f : 0.005f; currentBrake = Mathf.MoveTowards(currentBrake, targetBrake, data.brakeSmoothSpeed * Time.fixedDeltaTime); NoseWheelColliderL.brakeTorque = currentBrake; NoseWheelColliderL.motorTorque = 0.0001f; NoseWheelColliderR.brakeTorque = currentBrake; NoseWheelColliderR.motorTorque = 0.0001f; float curRot = -5 * controlState.currentYaw; curRot = Mathf.Clamp(curRot, -10, 10); tailWheelCollider.steerAngle = curRot; } }
4
3
2 комментария