Skip to content
This repository has been archived by the owner on Jan 5, 2024. It is now read-only.

Commit

Permalink
Merge pull request #576 from cortex-command-community/walkpath-auto-c…
Browse files Browse the repository at this point in the history
…rouch

Walkpath Auto Crouch
  • Loading branch information
Causeless authored Dec 29, 2023
2 parents 99a6ba2 + 8182cf2 commit 5d49daf
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 38 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- New `Actor` INI and Lua (R/W) property `PainThreshold`, which determines how much damage this actor must take in a frame to play their `PainSound`. This can be set to 0 to never manually play the sound. Defaults to 15.

- New `AHuman` INI and Lua (R/W) property `MaxWalkPathCrouchShift`, which determines how much the actor will automatically duck down to avoid low ceilings above them. This can be set to 0 to never duck. Defaults to 6.

- New `AHuman` INI and Lua (R/W) property `MaxCrouchRotation`, which determines how much the actor will rotate when ducking to avoid low ceilings above them. This can be set to 0 to never duck. Defaults to a quarter of Pi * 1.25 (roughly 56 degrees).

- New `AHuman` Lua (R/W) property `CrouchAmountOverride`, which enforces that the actor crouch a certain amount, where 0 means fully standing and 1 is fully crouching. This override can be disabled by setting it to -1.0.

- New `AHuman` Lua (R) property `CrouchAmount`, which returns how much the actor is crouching, where 0 means fully standing and 1 is fully crouching.

- New `MOPixel` INI and Lua (R/W) property `Staininess`, which defines how likely a pixel is to stain a surface when it collides with it. Staining a surface changes that surface's `Color` to that of this `MOPixel`, without changing the underlying material. Value can be between 0 and 1. Defaults to 0 (never stain).

- New `Activity` INI and Lua (R/W) property `AllowsUserSaving`, which can be used to enable/disable manual user saving/loading. This defaults to true for all `GAScripted` with an `OnSave()` function, but false otherwise. Lua `ActivityMan::SaveGame()` function now forces a save even if `AllowsUserSaving` is disabled. This allows mods and scripted gamemodes to handle saving in their own way (for example, only allowing saving at set points).
Expand Down
102 changes: 91 additions & 11 deletions Entities/AHuman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@
#include "GUI.h"
#include "AllegroBitmap.h"

#include "PrimitiveMan.h"

#include "tracy/Tracy.hpp"

namespace RTE {
Expand Down Expand Up @@ -63,6 +65,9 @@ void AHuman::Clear()
m_MoveState = STAND;
m_ProneState = NOTPRONE;
m_ProneTimer.Reset();
m_MaxWalkPathCrouchShift = 6.0F;
m_MaxCrouchRotation = c_QuarterPI * 1.25F;
m_CrouchAmountOverride = -1.0F;
for (int i = 0; i < MOVEMENTSTATECOUNT; ++i) {
m_Paths[FGROUND][i].Reset();
m_Paths[BGROUND][i].Reset();
Expand All @@ -85,6 +90,7 @@ void AHuman::Clear()
m_BGArmFlailScalar = 0.7F;
m_EquipHUDTimer.Reset();
m_WalkAngle.fill(Matrix());
m_WalkPathOffset.Reset();
m_ArmSwingRate = 1.0F;
m_DeviceArmSwayRate = 0.5F;

Expand Down Expand Up @@ -207,6 +213,9 @@ int AHuman::Create(const AHuman &reference) {
m_BackupBGFootGroup->SetOwner(this);
m_BackupBGFootGroup->SetLimbPos(atomGroupToUseAsFootGroupBG->GetLimbPos());

m_MaxWalkPathCrouchShift = reference.m_MaxWalkPathCrouchShift;
m_MaxCrouchRotation = reference.m_MaxCrouchRotation;

if (reference.m_StrideSound) { m_StrideSound = dynamic_cast<SoundContainer*>(reference.m_StrideSound->Clone()); }

m_ArmsState = reference.m_ArmsState;
Expand Down Expand Up @@ -283,6 +292,8 @@ int AHuman::ReadProperty(const std::string_view &propName, Reader &reader) {
m_BackupBGFootGroup = new AtomGroup(*m_pBGFootGroup);
m_BackupBGFootGroup->RemoveAllAtoms();
});
MatchProperty("MaxWalkPathCrouchShift", { reader >> m_MaxWalkPathCrouchShift; });
MatchProperty("MaxCrouchRotation", { reader >> m_MaxCrouchRotation; });
MatchProperty("StrideSound", {
m_StrideSound = new SoundContainer;
reader >> m_StrideSound;
Expand Down Expand Up @@ -346,6 +357,10 @@ int AHuman::Save(Writer &writer) const
writer << m_pFGFootGroup;
writer.NewProperty("BGFootGroup");
writer << m_pBGFootGroup;
writer.NewProperty("MaxWalkPathCrouchShift");
writer << m_MaxWalkPathCrouchShift;
writer.NewProperty("MaxCrouchRotation");
writer << m_MaxCrouchRotation;
writer.NewProperty("StrideSound");
writer << m_StrideSound;

Expand Down Expand Up @@ -1710,6 +1725,54 @@ void AHuman::UpdateWalkAngle(AHuman::Layer whichLayer) {

//////////////////////////////////////////////////////////////////////////////////////////

void AHuman::UpdateCrouching() {
if (!m_Controller.IsState(BODY_JUMP) && m_pHead) {
float desiredWalkPathYOffset = 0.0F;
if (m_CrouchAmountOverride == -1.0F) {
// Cast a ray above our head to either side to determine whether we need to crouch
float desiredCrouchHeadRoom = std::floor(m_pHead->GetRadius() + 2.0f);
float toPredicted = std::floor(m_Vel.m_X * m_pHead->GetRadius()); // Check where we'll be a second from now
Vector hitPosStart = (m_pHead->GetPos() + Vector(0.0F, m_SpriteRadius * 0.5F)).Floor();
Vector hitPosPredictedStart = (m_pHead->GetPos() + Vector(toPredicted, m_SpriteRadius * 0.5F)).Floor();
Vector hitPos, hitPosPredicted;
g_SceneMan.CastStrengthRay(hitPosStart, Vector(0.0F, -desiredCrouchHeadRoom + m_SpriteRadius * -0.5F), 1.0F, hitPos, 0, g_MaterialGrass);
g_SceneMan.CastStrengthRay(hitPosPredictedStart, Vector(0.0F, -desiredCrouchHeadRoom + m_SpriteRadius * -0.5F), 1.0F, hitPosPredicted, 0, g_MaterialGrass);

// Don't do it if we're already hitting, we're probably in a weird spot
if (hitPosStart.m_Y - hitPos.m_Y <= 2.0F) {
hitPos.m_Y = 0.0F;
}

if (hitPosPredictedStart.m_Y - hitPosPredicted.m_Y <= 2.0F) {
hitPosPredicted.m_Y = 0.0F;
}

float headroom = m_pHead->GetPos().m_Y - std::max(hitPos.m_Y, hitPosPredicted.m_Y);
desiredWalkPathYOffset = desiredCrouchHeadRoom - headroom;
} else {
desiredWalkPathYOffset = m_CrouchAmountOverride * m_MaxWalkPathCrouchShift;
}

float finalWalkPathYOffset = std::clamp(LERP(0.0F, 1.0F, -m_WalkPathOffset.m_Y, desiredWalkPathYOffset, 0.3F), 0.0F, m_MaxWalkPathCrouchShift);
m_WalkPathOffset.m_Y = -finalWalkPathYOffset;

// If crouching, move at reduced speed
const float crouchSpeedMultiplier = 0.5F;
float travelSpeedMultiplier = LERP(0.0F, m_MaxWalkPathCrouchShift, 1.0F, crouchSpeedMultiplier, -m_WalkPathOffset.m_Y);
m_Paths[FGROUND][WALK].SetTravelSpeedMultiplier(travelSpeedMultiplier);
m_Paths[BGROUND][WALK].SetTravelSpeedMultiplier(travelSpeedMultiplier);

// Adjust our X offset to try to keep our legs under our centre-of-mass
const float ratioBetweenBodyAndHeadToAimFor = 0.15F;
float predictedPosition = ((m_pHead->GetPos().m_X - m_Pos.m_X) * ratioBetweenBodyAndHeadToAimFor) + m_Vel.m_X;
m_WalkPathOffset.m_X = predictedPosition;
} else {
m_WalkPathOffset.Reset();
}
}

//////////////////////////////////////////////////////////////////////////////////////////

void AHuman::PreControllerUpdate()
{
ZoneScoped;
Expand Down Expand Up @@ -2182,6 +2245,8 @@ void AHuman::PreControllerUpdate()

m_StrideFrame = false;

UpdateCrouching();

if (m_Status == STABLE && !m_LimbPushForcesAndCollisionsDisabled && m_MoveState != NOMOVE)
{
// This exists to support disabling foot collisions if the limbpath has that flag set.
Expand All @@ -2194,6 +2259,9 @@ void AHuman::PreControllerUpdate()
std::swap(m_pBGFootGroup, m_BackupBGFootGroup);
}

if (m_pFGLeg) { UpdateWalkAngle(FGROUND); }
if (m_pBGLeg) { UpdateWalkAngle(BGROUND); }

// WALKING, OR WE ARE JETPACKING AND STUCK
if (m_MoveState == WALK || (m_MoveState == JUMP && isStill)) {
m_Paths[FGROUND][STAND].Terminate();
Expand All @@ -2212,17 +2280,17 @@ void AHuman::PreControllerUpdate()
if (m_pFGLeg && (!m_pBGLeg || !(m_Paths[FGROUND][WALK].PathEnded() && BGLegProg < 0.5F) || m_StrideStart)) {
// Reset the stride timer if the path is about to restart.
if (m_Paths[FGROUND][WALK].PathEnded() || m_Paths[FGROUND][WALK].PathIsAtStart()) { m_StrideTimer.Reset(); }
m_ArmClimbing[BGROUND] = !m_pFGFootGroup->PushAsLimb(m_Pos + RotateOffset(m_pFGLeg->GetParentOffset()), m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[FGROUND][WALK].GetLowestY()));
if (restarted) { UpdateWalkAngle(FGROUND); }
Vector jointPos = m_Pos + RotateOffset(m_pFGLeg->GetParentOffset());
m_ArmClimbing[BGROUND] = !m_pFGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[FGROUND][WALK].GetLowestY()), m_WalkPathOffset);
} else {
m_ArmClimbing[BGROUND] = false;
}
if (m_pBGLeg && (!m_pFGLeg || !(m_Paths[BGROUND][WALK].PathEnded() && FGLegProg < 0.5F))) {
m_StrideStart = false;
// Reset the stride timer if the path is about to restart.
if (m_Paths[BGROUND][WALK].PathEnded() || m_Paths[BGROUND][WALK].PathIsAtStart()) { m_StrideTimer.Reset(); }
m_ArmClimbing[FGROUND] = !m_pBGFootGroup->PushAsLimb(m_Pos + RotateOffset(m_pBGLeg->GetParentOffset()), m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[BGROUND][WALK].GetLowestY()));
if (restarted) { UpdateWalkAngle(BGROUND); }
Vector jointPos = m_Pos + RotateOffset(m_pBGLeg->GetParentOffset());
m_ArmClimbing[FGROUND] = !m_pBGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][WALK], deltaTime, &restarted, false, Vector(0.0F, m_Paths[BGROUND][WALK].GetLowestY()), m_WalkPathOffset);
} else {
if (m_pBGLeg) { m_pBGFootGroup->FlailAsLimb(m_Pos, RotateOffset(m_pBGLeg->GetParentOffset()), m_pBGLeg->GetMaxLength(), m_PrevVel, m_AngularVel, m_pBGLeg->GetMass(), deltaTime); }
m_ArmClimbing[FGROUND] = false;
Expand Down Expand Up @@ -2360,9 +2428,15 @@ void AHuman::PreControllerUpdate()
m_Paths[FGROUND][ARMCRAWL].Terminate();
m_Paths[BGROUND][ARMCRAWL].Terminate();

if (m_pFGLeg) { m_pFGFootGroup->PushAsLimb(m_Pos.GetFloored() + m_pFGLeg->GetParentOffset().GetXFlipped(m_HFlipped), m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][STAND], deltaTime, nullptr, !m_pBGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY())); }
if (m_pFGLeg) {
Vector jointPos = m_Pos.GetFloored() + m_pFGLeg->GetParentOffset().GetXFlipped(m_HFlipped);
m_pFGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[FGROUND], m_Paths[FGROUND][STAND], deltaTime, nullptr, !m_pBGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY()), m_WalkPathOffset);
}

if (m_pBGLeg) { m_pBGFootGroup->PushAsLimb(m_Pos.GetFloored() + m_pBGLeg->GetParentOffset().GetXFlipped(m_HFlipped), m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][STAND], deltaTime, nullptr, !m_pFGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY())); }
if (m_pBGLeg) {
Vector jointPos = m_Pos.GetFloored() + m_pBGLeg->GetParentOffset().GetXFlipped(m_HFlipped);
m_pBGFootGroup->PushAsLimb(jointPos, m_Vel, m_WalkAngle[BGROUND], m_Paths[BGROUND][STAND], deltaTime, nullptr, !m_pFGLeg, Vector(0.0F, m_Paths[FGROUND][STAND].GetLowestY()), m_WalkPathOffset);
}
}
}
}
Expand Down Expand Up @@ -2613,7 +2687,13 @@ void AHuman::Update()
}
} else {
// Upright body posture
float rotDiff = rot - (GetRotAngleTarget(m_MoveState) * (m_AimAngle > 0 ? 1.0F - (m_AimAngle / c_HalfPI) : 1.0F) * GetFlipFactor());
float rotTarget = (GetRotAngleTarget(m_MoveState) * (m_AimAngle > 0 ? 1.0F - (m_AimAngle / c_HalfPI) : 1.0F) * GetFlipFactor());

// Lean forwards when crouching
float crouchAngleAdjust = m_HFlipped ? m_MaxCrouchRotation : -m_MaxCrouchRotation;
rotTarget += LERP(0.0F, m_MaxWalkPathCrouchShift, 0.0F, crouchAngleAdjust, m_WalkPathOffset.m_Y * -1.0F);

float rotDiff = rot - rotTarget;
m_AngularVel = m_AngularVel * (0.98F - 0.06F * (m_Health / m_MaxHealth)) - (rotDiff * 0.5F);
}
}
Expand Down Expand Up @@ -2722,10 +2802,10 @@ void AHuman::Draw(BITMAP *pTargetBitmap, const Vector &targetPos, DrawMode mode,
}

if (mode == g_DrawColor && !onlyPhysical && g_SettingsMan.DrawLimbPathVisualizations()) {
m_Paths[m_HFlipped][WALK].Draw(pTargetBitmap, targetPos, 122);
m_Paths[m_HFlipped][CRAWL].Draw(pTargetBitmap, targetPos, 122);
m_Paths[m_HFlipped][ARMCRAWL].Draw(pTargetBitmap, targetPos, 13);
m_Paths[m_HFlipped][CLIMB].Draw(pTargetBitmap, targetPos, 165);
m_Paths[m_HFlipped][WALK].Draw(pTargetBitmap, targetPos, 122);
m_Paths[m_HFlipped][CRAWL].Draw(pTargetBitmap, targetPos, 122);
m_Paths[m_HFlipped][ARMCRAWL].Draw(pTargetBitmap, targetPos, 13);
m_Paths[m_HFlipped][CLIMB].Draw(pTargetBitmap, targetPos, 165);
}
}

Expand Down
54 changes: 54 additions & 0 deletions Entities/AHuman.h
Original file line number Diff line number Diff line change
Expand Up @@ -688,6 +688,11 @@ DefaultPieMenuNameGetter("Default Human Pie Menu");
/// <param name="whichLayer">The Layer in question.</param>
void UpdateWalkAngle(AHuman::Layer whichLayer);

/// <summary>
/// Detects overhead ceilings and crouches for them.
/// </summary>
void UpdateCrouching();

/// <summary>
/// Gets the walk path rotation for the specified Layer.
/// </summary>
Expand Down Expand Up @@ -863,6 +868,48 @@ DefaultPieMenuNameGetter("Default Human Pie Menu");
/// <param name="newValue">The new device arm sway rate for this AHuman.</param>
void SetDeviceArmSwayRate(float newValue) { m_DeviceArmSwayRate = newValue; }

/// <summary>
/// Gets this AHuman's max walkpath adjustment upwards to crouch below low ceilings.
/// </summary>
/// <returns>This AHuman's max walkpath adjustment.</returns>
float GetMaxWalkPathCrouchShift() const { return m_MaxWalkPathCrouchShift; }

/// <summary>
/// Sets this AHuman's max walkpath adjustment upwards to crouch below low ceilings.
/// </summary>
/// <param name="newValue">The new value for this AHuman's max walkpath adjustment.</param>
void SetMaxWalkPathCrouchShift(float newValue) { m_MaxWalkPathCrouchShift = newValue; }

/// <summary>
/// Gets this AHuman's max crouch rotation to duck below low ceilings.
/// </summary>
/// <returns>This AHuman's max crouch rotation adjustment.</returns>
float GetMaxCrouchRotation() const { return m_MaxCrouchRotation; }

/// <summary>
/// Sets this AHuman's max crouch rotation to duck below low ceilings.
/// </summary>
/// <param name="newValue">The new value for this AHuman's max crouch rotation adjustment.</param>
void SetMaxCrouchRotation(float newValue) { m_MaxCrouchRotation = newValue; }

/// <summary>
/// Gets this AHuman's current crouch amount. 0.0 == fully standing, 1.0 == fully crouched.
/// </summary>
/// <returns>This AHuman's current crouch amount.</returns>
float GetCrouchAmount() const { return (m_WalkPathOffset.m_Y * -1.0F) / m_MaxWalkPathCrouchShift; }

/// <summary>
/// Gets this AHuman's current crouch amount override. 0.0 == fully standing, 1.0 == fully crouched, -1 == no override.
/// </summary>
/// <returns>This AHuman's current crouch amount override.</returns>
float GetCrouchAmountOverride() const { return m_CrouchAmountOverride; }

/// <summary>
/// Sets this AHuman's current crouch amount override.
/// </summary>
/// <param name="newValue">The new value for this AHuman's current crouch amount override.</param>
void SetCrouchAmountOverride(float newValue) { m_CrouchAmountOverride = newValue; }

/// <summary>
/// Gets this AHuman's stride sound. Ownership is NOT transferred!
/// </summary>
Expand Down Expand Up @@ -935,6 +982,12 @@ DefaultPieMenuNameGetter("Default Human Pie Menu");
ProneState m_ProneState;
// Timer for the going prone procedural animation
Timer m_ProneTimer;
// The maximum amount our walkpath can be shifted upwards to crouch and avoid ceilings above us
float m_MaxWalkPathCrouchShift;
// The maximum amount we will duck our head down to avoid obstacles above us.
float m_MaxCrouchRotation;
// The script-set forced crouching amount. 0.0 == fully standing, 1.0 == fully crouched, -1 == no override.
float m_CrouchAmountOverride;
// Limb paths for different movement states.
// [0] is for the foreground limbs, and [1] is for BG.
LimbPath m_Paths[2][MOVEMENTSTATECOUNT];
Expand All @@ -958,6 +1011,7 @@ DefaultPieMenuNameGetter("Default Human Pie Menu");
float m_BGArmFlailScalar; //!< The rate at which this AHuman's BG Arm follows the the bodily rotation. Set to a negative value for a "counterweight" effect.
Timer m_EquipHUDTimer; //!< Timer for showing the name of any newly equipped Device.
std::array<Matrix, 2> m_WalkAngle; //!< An array of rot angle targets for different movement states.
Vector m_WalkPathOffset;
float m_ArmSwingRate; //!< Controls the rate at which this AHuman's Arms follow the movement of its Legs while they're not holding device(s).
float m_DeviceArmSwayRate; //!< Controls the rate at which this AHuman's Arms follow the movement of its Legs while they're holding device(s). One-handed devices sway half as much as two-handed ones. Defaults to three quarters of Arm swing rate.

Expand Down
11 changes: 8 additions & 3 deletions Entities/AtomGroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1214,7 +1214,7 @@ namespace RTE {

/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

bool AtomGroup::PushAsLimb(const Vector &jointPos, const Vector &velocity, const Matrix &rotation, LimbPath &limbPath, const float travelTime, bool *restarted, bool affectRotation, Vector rotationOffset) {
bool AtomGroup::PushAsLimb(const Vector &jointPos, const Vector &velocity, const Matrix &rotation, LimbPath &limbPath, const float travelTime, bool *restarted, bool affectRotation, Vector rotationOffset, Vector positionOffset) {
RTEAssert(m_OwnerMOSR, "Tried to push-as-limb an AtomGroup that has no parent!");

bool didWrap = false;
Expand All @@ -1234,6 +1234,7 @@ namespace RTE {
limbPath.SetJointVel(velocity);
limbPath.SetRotation(rotation);
limbPath.SetRotationOffset(rotationOffset);
limbPath.SetPositionOffset(positionOffset);
limbPath.SetFrameTime(travelTime);

Vector limbDist = g_SceneMan.ShortestDistance(adjustedJointPos, m_LimbPos, g_SceneMan.SceneWrapsX());
Expand Down Expand Up @@ -1266,9 +1267,13 @@ namespace RTE {
owner->GetController()->IsState(MOVE_RIGHT) && pushImpulse.m_X < 0.0F;
if (againstTravelDirection) {
// Filter some of our impulse out. We're pushing against an obstacle, but we don't want to kick backwards!
// Translate it into to upwards motion to step over what we're walking into instead ;)
const float againstIntendedDirectionMultiplier = 0.5F;
pushImpulse.m_Y -= std::abs(pushImpulse.m_X * (1.0F - againstIntendedDirectionMultiplier));

if (!owner->GetController()->IsState(BODY_CROUCH) && !owner->GetController()->IsState(MOVE_DOWN)) {
// Translate it into to upwards motion to step over what we're walking into instead ;)
pushImpulse.m_Y -= std::abs(pushImpulse.m_X * (1.0F - againstIntendedDirectionMultiplier));
}

pushImpulse.m_X *= againstIntendedDirectionMultiplier;
}
}
Expand Down
3 changes: 2 additions & 1 deletion Entities/AtomGroup.h
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,9 @@ namespace RTE {
/// <param name="restarted">Pointer to a bool which gets set to true if the LimbPath got restarted during this push. It does NOT get initialized to false!</param>
/// <param name="affectRotation">Whether the forces created by this should have rotational leverage on the owner or only have translational effect.</param>
/// <param name="rotationOffset">The position, relative to the owning actor's position, that we should rotate around.</param>
/// <param name="rotationOffset">The positional offset to apply to our limb path.</param>
/// <returns>Whether the LimbPath passed in could start free of terrain or not.</returns>
bool PushAsLimb(const Vector &jointPos, const Vector &velocity, const Matrix &rotation, LimbPath &limbPath, const float travelTime, bool *restarted = nullptr, bool affectRotation = true, Vector rotationOffset = Vector());
bool PushAsLimb(const Vector &jointPos, const Vector &velocity, const Matrix &rotation, LimbPath &limbPath, const float travelTime, bool *restarted = nullptr, bool affectRotation = true, Vector rotationOffset = Vector(), Vector positionOffset = Vector());

/// <summary>
/// Makes this AtomGroup travel as a lifeless limb, constrained to a radius around the joint pin in the center.
Expand Down
Loading

0 comments on commit 5d49daf

Please sign in to comment.