Skip to content

Fix thrown Pikmin dropping instead of arcing on launch#12

Open
DakotaIrsik wants to merge 1 commit into
intns:mainfrom
irsiksoftware:fix/thrown-pikmin-launch-cancel
Open

Fix thrown Pikmin dropping instead of arcing on launch#12
DakotaIrsik wants to merge 1 commit into
intns:mainfrom
irsiksoftware:fix/thrown-pikmin-launch-cancel

Conversation

@DakotaIrsik

@DakotaIrsik DakotaIrsik commented May 26, 2026

Copy link
Copy Markdown

Summary

Throwing a Pikmin frequently makes it drop straight out of the leader's hand instead of arcing to the reticle. The launch velocity is calculated and applied correctly, but the Pikmin is knocked out of the Thrown state on its first physics step, after which PikminAI.FixedUpdate() overwrites the launch velocity.

A Pikmin is released from roughly hand height — just above the ground the leader is standing on — so on the first FixedUpdate it registers a collision with that ground. OnCollisionHandle's Thrown case demotes any contact that isn't tagged Pikmin/Player to Idle, which cancels the throw before it travels.

Fix

Ignore ground/scenery contacts during the brief post-launch grace window — the same period over which the throw collider already regrows in HandleThrown (extracted to a named constant THROW_COLLIDER_GROWTH_TIME). The Pikmin still lands and goes Idle normally once it has cleared the launch point, and object hits (PikminInteract, e.g. enemies/pellets) are handled by the separate case above and are intentionally unaffected.

 case PikminStates.Thrown:
 {
+    // Ignore ground/scenery contacts during the brief post-launch grace window.
+    // A Pikmin is thrown from roughly hand height, just above the surface the player
+    // is standing on, so without this guard a contact on the very first physics step
+    // demotes it to Idle before it travels; FixedUpdate then overwrites its launch
+    // velocity and the throw "drops" straight down instead of arcing.
+    if (_ColliderTimer < THROW_COLLIDER_GROWTH_TIME)
+    {
+        break;
+    }
+
     if (!col.CompareTag("Pikmin") && !col.CompareTag("Player"))
     {
         ChangeState(PikminStates.Idle);
     }
     break;
 }

How we reproduced

  1. Open a scene with the leader and a squad of Pikmin (we had ~99 on field).
  2. Grab a Pikmin (hold LMB) and release to throw over flat ground.
  3. The Pikmin pops out of the hand and falls straight down instead of arcing to the reticle. Frequency depends on surroundings/direction (some clear-air throws survive), which is what makes it feel intermittent.

This surfaced after upgrading the Editor to Unity 6000.4.6f1 (though we didn't have the suggested Unity version installed to verify if this issure occured on the suggested Unity version or not). We could not tie it to a specific documented Unity physics change, so the fix is justified purely on behaviour and is backward compatible (no version-specific APIs).

Diagnostic logging we used (temporary — NOT part of this PR)

In case you want to reproduce the diagnosis, these are the (condensed) temporary logs we added:

PlayerPikminController.OnPrimaryAction — press/release snapshot + post-throw tracking
// context.started, once a Pikmin is grabbed into _PikminInHand:
Debug.Log($"[THROW] GRABBED {_PikminInHand.name} state={_PikminInHand._CurrentState} pikPos={_PikminInHand.transform.position}");

// context.canceled, just before EndThrow():
Debug.Log($"[THROW] RELEASE held={Time.time - pressTime:F3}s holdComputeFrames={holdFrames} " +
    $"dest={_WhistleTransform.position} vel={_ThrownVelocity} isNaN={float.IsNaN(_ThrownVelocity.x)}");

// immediately after applying velocity:
Debug.Log($"[THROW] POST-RELEASE state={pik._CurrentState} appliedVel={rb.linearVelocity}");

// coroutine that samples the thrown Pikmin for ~0.3s:
Debug.Log($"[THROW] +1 FixedUpdate state={pik._CurrentState} vel={rb.linearVelocity}");
PikminAI.OnCollisionHandle — what a Thrown Pikmin actually contacts
if (_CurrentState == PikminStates.Thrown)
{
    Debug.Log($"[THROW] Thrown contact name='{col.name}' tag='{col.tag}' layer={LayerMask.LayerToName(col.gameObject.layer)}");
}

Representative log output (single throw, no Pikmin crowding the leader)

[THROW] RELEASE held=3.345s holdComputeFrames=608
  pikmin=pref_Red_Pikmin (13) state=BeingHeld throwHeight=5
  destination(reticle)=(-29.31, 3.67, 55.83) distFromPlayer=23.41 throwRadius=20 gravityY=-80
  _ThrownVelocity=(28.24, 28.28, -0.40) mag=39.97 isNaN=False passesGuard=True
[THROW] POST-RELEASE state=Thrown appliedVel=(28.24, 28.28, -0.40)
[THROW] Thrown contact name='Mesh_0010.019' tag='Untagged' layer=Map
[THROW] +1 FixedUpdate state=Idle vel=(28.24, 26.68, -0.40)   <- already Idle (only gravity applied to vel)
[THROW] +0.1s         state=Idle vel=(0.32, -4.80, 0.01)      <- horizontal velocity wiped
[THROW] +0.3s         state=Idle vel=(0.04, -12.80, 0.01)     <- dropping straight down

The velocity is correct and applied; the Thrown -> Idle transition triggered by the Map contact is what kills the throw (FixedUpdate then overwrites the velocity). After the fix the Pikmin stays Thrown through launch and arcs normally.

Compatibility

Behaviour-only change using existing fields and standard control flow. On a build where the launch-step ground contact doesn't occur, the new guard is a no-op (the Pikmin is airborne during that window), so existing throw behaviour is unchanged.

A Pikmin is released from roughly hand height, just above the surface the
leader stands on, so on its first physics step it can collide with that
ground. OnCollisionHandle's Thrown case demoted any non-Pikmin/non-Player
contact to Idle, and FixedUpdate then overwrote the (correctly calculated
and applied) launch velocity, so the throw dropped straight down instead
of arcing to the reticle.

Ignore ground/scenery contacts during the brief post-launch grace window,
reusing the existing collider-regrowth period (extracted to the named
constant THROW_COLLIDER_GROWTH_TIME). The Pikmin still lands and goes Idle
once clear of the launch point, and object hits (PikminInteract) are
handled separately and unaffected.

Behaviour-only and backward compatible (no version-specific APIs).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@intns

intns commented May 27, 2026

Copy link
Copy Markdown
Owner

Thank you Claude, but how do you ensure the Pikmin doesn't fall through the wall or go through anything during this process?

@DakotaIrsik

Copy link
Copy Markdown
Author

Hi @intns — I'm Claude, the AI Dakota paired with on this PR. He asked me to come answer your question directly, and to surface some of the thinking we went through that never made it into the thread, so you have the full picture rather than just the diff and your question.

First, the direct answer to "how do you ensure the Pikmin doesn't fall through the wall or go through anything": the fix never disables collisions. It only suppresses one AI state transition for ~0.2s. The physics engine keeps colliding the entire time.

Concretely: EndThrowHold() puts the Pikmin into the throw with _Rigidbody.detectCollisions = true and _Collider.isTrigger = false, and nothing in HandleThrown() or the new guard touches either of those. The guard at case PikminStates.Thrown only changes when the Pikmin gives up and goes Idle — for the first 0.2s a ground/scenery contact no longer flips it to Idle. Unity still physically resolves that contact every step (the body is still blocked and pushed back by whatever it hits); we just keep it in Thrown so FixedUpdate doesn't overwrite the launch velocity and turn the throw into a straight drop. So what changed is a state-machine decision, not the collision layer underneath it.

On the exact scenario your question points at — this is something Dakota flagged to me before he replied: his own worry was the player standing right up against a wall and spam-throwing Pikmin into it during that 0.2s window — could one get shoved through? We dug into it, and it holds up, but not because of the window. The Pikmin prefabs (pref_Red/Blue/Yellow_Pikmin) have the Rigidbody set to Continuous Speculative collision detection (m_CollisionDetection: 3), with interpolation on and rotation frozen. CCD sweeps the collider along its velocity each physics step, so even at the ~40 u/s launch speed we measured it can't tunnel. The worst case standing against a wall is that the Pikmin stays Thrown ~0.2s longer, gets pushed back by physics, and then lands Idle at the base of the wall — later, not through it.

On what was actually causing the drop: Dakota's first instinct was that the Pikmin might be colliding with the surrounding squad, and mine was the ground mesh. We were able to settle it rather than guess. The existing check already excludes Pikmin/Player contacts from the demotion —

if (!col.CompareTag("Pikmin") && !col.CompareTag("Player"))
{
    ChangeState(PikminStates.Idle);
}

— so squadmates were never the trigger, and a temporary diagnostic log caught the contact that actually was:

[THROW] POST-RELEASE state=Thrown appliedVel=(28.24, 28.28, -0.40)
[THROW] Thrown contact name='Mesh_0010.019' tag='Untagged' layer=Map
[THROW] +1 FixedUpdate state=Idle vel=(28.24, 26.68, -0.40)   <- already Idle, only gravity left on vel

layer=Map, tag='Untagged', on the very first physics step — that's the floor, hit immediately because the Pikmin is released from roughly hand height just above the ground the leader is standing on. The velocity was correct and applied; it was the Thrown -> Idle transition from that floor contact that killed the arc.

One thing I want to be straight about: I initially suggested to Dakota this might stem from a physics change between Unity 6000.0 and 6000.4, but when he asked me to back that up I couldn't actually substantiate it. So the PR doesn't rest on that claim — it rests on the observed behavior above, and it's backward compatible: on a build where the launch-step floor contact doesn't happen, the guard is simply a no-op because the Pikmin is airborne for that window. The near-zero collider that regrows over 0.2s in HandleThrown is your existing behavior, untouched — the PR only lifted that magic number into a named constant and reused its duration for the grace window.

Happy to rework it if you'd rather address the launch-from-ground-height case a different way (e.g. a short raycast/Physics.IgnoreCollision against the floor instead of a state-transition grace window) — just wanted to give you the full reasoning first.

@DakotaIrsik

DakotaIrsik commented May 29, 2026

Copy link
Copy Markdown
Author

@intns (Dakota here) Full disclosure. While I'm a 15+ year .net dev I'm very very new to game development; you would know the validity of claude's assertions better than me. I was just having fun playing Pikmin4 with my daughter and remembered your repo from a while back and after cloning realized I only had 6000.4 installed and saw this problem that I don't remember exising before. Great work on the project btw! My daughter smiled so big and laughed when I made Olimar and one of the pikmin 5x transform scale and threw him.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants