//===========================================================================// // File: mech2.cpp // // Project: BattleTech Brick: Entity Manager // // Contents: Mech gait animation -- the transition machine // //---------------------------------------------------------------------------// // Date Who Modification // // -------- --- ---------------------------------------------------------- // // // //---------------------------------------------------------------------------// // Copyright (C) 1995, Virtual World Entertainment, Inc. // // All Rights reserved worldwide // // This unpublished sourcecode is PROPRIETARY and CONFIDENTIAL // //===========================================================================// #include #pragma hdrstop #if !defined(MECH_HPP) # include #endif #if !defined(MECHMPPR_HPP) # include #endif // //############################################################################# // A mech walks on two parallel clip channels. // // The LEG channel is the locally-simulated gait. Its transitions read the // LIVE commanded speed out of the controls mapper, so it responds to the // stick the instant it moves. // // The BODY channel is the displayed motion, and the distance IT advances is // what carries the mech forward. Its transitions read bodyTargetSpeed -- // a snapshot -- which is what lets a dead-reckoned or networked mech walk // properly with no controls mapper of its own. // // Both channels run the same state machine over the same clips; only the // speed they consult differs. That is the whole reason the two ClipFinished // functions below are near-twins rather than one shared routine, and the // symmetry is load-bearing: where the binary's two jump tables agree, a // disagreement in this file is a bug. // // Each clip is ONE STRIDE, which is why every state is handed. A walk is // Right, Left, Right, ... and each entry to and exit from a cycle has its own // handed pair so the mech always leaves on the correct foot. // // The machine only ever runs at END OF CLIP. SequenceController::Advance // calls the channel's finished callback, which picks the next state, re-arms // the channel, and spends the leftover time in the new clip -- returning the // distance that leftover covered so Advance can fold it in. Getting that // contract wrong double-counts the mech's forward motion. //############################################################################# // // //############################################################################# // @004a7fc4 -- bind the leg channel to a state's clip and record the state. //############################################################################# // void Mech::SetLegAnimation(int state) { Check(this); // // Bounded by the SLOT count, not the name count: slot 0x20 is the // bump/crash clip and is legitimately bound on a wall impact. // Verify(state >= 0 && state < AnimationSlotCount); legAnimation.SelectSequence( animationClips[state], (void *)Mech::LegClipFinished, 0, 0); legStateAlarm.SetLevel((unsigned)state); } // //############################################################################# // @004a800c -- the body channel's equivalent. // // This one also drives the animation StateIndicators, which is how the audio // subsystem's watchers learn a gait changed. Guarded to the constructed // range so an out-of-range clip cannot trip StateIndicator's own Verify. //############################################################################# // void Mech::SetBodyAnimation(int state) { Check(this); Verify(state >= 0 && state < AnimationSlotCount); bodyAnimation.SelectSequence( animationClips[state], (void *)Mech::BodyClipFinished, 0, 0); bodyStateAlarm.SetLevel((unsigned)state); animationState.SetState(state); replicantAnimationState.SetState(state); } // //############################################################################# // The shared tails (@0x4a6a06 leg / @0x4a6e66 body) every handler ends in: // bind the next state, then spend the carryover inside it. //############################################################################# // Scalar Mech::LegTransition(int next_state, Scalar advance_time, int move_joints) { Check(this); SetLegAnimation(next_state); return legAnimation.Advance(advance_time, move_joints); } Scalar Mech::BodyTransition(int next_state, Scalar advance_time, int move_joints) { Check(this); SetBodyAnimation(next_state); return bodyAnimation.Advance(advance_time, move_joints); } // //############################################################################# // @004a6928 -- the LEG channel's end-of-clip machine (jump table @0x4a69aa). // // Reads the live commanded speed from the controls mapper. A mech with no // mapper reads zero and simply idles, which is the correct behaviour for a // replicant. //############################################################################# // Scalar Mech::LegClipFinished( Mech *mech, unsigned /* callback_arg */, Scalar carryover, int move_joints ) { Check(mech); // // The binary reads subsystemArray[0] -- the roster's controls-mapper slot. // A mech without one (a replicant) reads zero and idles, which is right. // Scalar demand = 0.0f; if (mech->subsystemArray != NULL && mech->subsystemArray[0] != NULL) { demand = ((MechControlsMapper *)mech->subsystemArray[0])->GetSpeedDemand(); } Scalar cycle_rate = mech->forwardCycleRate, time_scale = mech->globalTimeScale, cycle = mech->legCycleSpeed, tail_time = carryover * time_scale; switch (mech->legStateAlarm.GetLevel()) { // // Standing and the idle group -- nothing to transition to. // case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27: return 0.0f; case 2: mech->legStateAlarm.SetLevel(1); return 0.0f; // // The transition-END clips: having arrived, fall back to standing. // case 3: case 4: case 8: case 9: case 20: case 21: case 28: case 29: case 30: case 31: case 32: mech->legStateAlarm.SetLevel(0); return 0.0f; // // Walking, right foot down (@0x4a6aad). Three ways out: stop if the // demand has fallen below the "moving at all" threshold, step up toward // the run cycle if it is over the walk cap, otherwise take the next // stride on the other foot. // // The stop and step-up tests each check the DEMAND and the CURRENT CYCLE // SPEED slewed by one carryover -- so a momentary flick of the stick // cannot yank the mech out of a stride it has already committed to. // case 5: case 6: case 14: if ( demand < mech->standSpeed && (cycle - cycle_rate * carryover) < mech->standSpeed ) { return mech->LegTransition(9, tail_time, move_joints); } if ( demand > mech->walkStrideLength && (cycle + cycle_rate * carryover) > mech->walkStrideLength ) { return mech->LegTransition(0xb, tail_time, move_joints); } return mech->LegTransition( 7, carryover * cycle * time_scale / mech->walkStrideLength, move_joints); // // Walking, left foot down (@0x4a69d6) -- the mirror. // case 7: case 15: if ( demand < mech->standSpeed && (cycle - cycle_rate * carryover) < mech->standSpeed ) { return mech->LegTransition(8, tail_time, move_joints); } if ( demand > mech->walkStrideLength && (cycle + cycle_rate * carryover) > mech->walkStrideLength ) { return mech->LegTransition(0xa, tail_time, move_joints); } return mech->LegTransition( 6, carryover * cycle * time_scale / mech->walkStrideLength, move_joints); // // Running / reversing (@0x4a6bdb and @0x4a6b63): drop back to the walk // cycle when the demand decays, else alternate feet. // case 10: case 12: if ( demand < mech->reverseSpeedMax && (cycle - cycle_rate * carryover) < mech->reverseSpeedMax ) { return mech->LegTransition(0xf, tail_time, move_joints); } return mech->LegTransition( 0xd, carryover * cycle * time_scale / mech->reverseStrideLength, move_joints); case 11: case 13: if ( demand < mech->reverseSpeedMax && (cycle - cycle_rate * carryover) < mech->reverseSpeedMax ) { return mech->LegTransition(0xe, tail_time, move_joints); } return mech->LegTransition( 0xc, carryover * cycle * time_scale / mech->reverseStrideLength, move_joints); // // Limping (@0x4a6c17 and @0x4a6cc4). gimpStrideLength is authored // NEGATIVE, so the cycle time comes out negative and has to be folded // positive before it can be spent -- the binary does exactly this at // @0x4a6c6e / @0x4a6d3d. // case 16: case 18: if ( demand > mech->gimpSpeedMax && (mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax ) { return mech->LegTransition(0x15, tail_time, move_joints); } { Scalar cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength; if (cycle_time <= 0.0f) { cycle_time = -cycle_time; } return mech->LegTransition(0x13, cycle_time, move_joints); } case 17: case 19: if ( demand > mech->gimpSpeedMax && (mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax ) { return mech->LegTransition(0x14, tail_time, move_joints); } { Scalar cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength; if (cycle_time <= 0.0f) { cycle_time = -cycle_time; } return mech->LegTransition(0x12, cycle_time, move_joints); } } // // Falls, crashes and the death clips play out and stop here. // return 0.0f; } // //############################################################################# // @004a6d8c -- the BODY channel's end-of-clip machine (jump table @0x4a6e0a). // // Structurally identical to the leg machine above, reading bodyTargetSpeed // instead of the live mapper demand. Kept as its own routine because that is // how the binary has it, and because the two tables are each other's check. //############################################################################# // Scalar Mech::BodyClipFinished( Mech *mech, unsigned /* callback_arg */, Scalar carryover, int move_joints ) { Check(mech); Scalar cycle_rate = mech->forwardCycleRate, time_scale = mech->globalTimeScale, cycle = mech->bodyCycleSpeed, demand = mech->bodyTargetSpeed, tail_time = carryover * time_scale; switch (mech->bodyStateAlarm.GetLevel()) { case 0: case 1: case 22: case 23: case 24: case 25: case 26: case 27: return 0.0f; case 2: mech->bodyStateAlarm.SetLevel(1); return 0.0f; case 3: case 4: case 8: case 9: case 20: case 21: case 28: case 29: case 30: case 31: case 32: mech->bodyStateAlarm.SetLevel(0); return 0.0f; // // Walking, right foot down (@0x4a6f11). // case 5: case 6: case 14: if ( demand < mech->standSpeed && (cycle - cycle_rate * carryover) < mech->standSpeed ) { return mech->BodyTransition(9, tail_time, move_joints); } if ( demand > mech->walkStrideLength && (cycle + cycle_rate * carryover) > mech->walkStrideLength ) { return mech->BodyTransition(0xb, tail_time, move_joints); } return mech->BodyTransition( 7, carryover * cycle * time_scale / mech->walkStrideLength, move_joints); // // Walking, left foot down (@0x4a6e36). // case 7: case 15: if ( demand < mech->standSpeed && (cycle - cycle_rate * carryover) < mech->standSpeed ) { return mech->BodyTransition(8, tail_time, move_joints); } if ( demand > mech->walkStrideLength && (cycle + cycle_rate * carryover) > mech->walkStrideLength ) { return mech->BodyTransition(0xa, tail_time, move_joints); } return mech->BodyTransition( 6, carryover * cycle * time_scale / mech->walkStrideLength, move_joints); // // Running / reversing (@0x4a7041 and @0x4a6fc7). // case 10: case 12: if ( demand < mech->reverseSpeedMax && (cycle - cycle_rate * carryover) < mech->reverseSpeedMax ) { return mech->BodyTransition(0xf, tail_time, move_joints); } return mech->BodyTransition( 0xd, carryover * cycle * time_scale / mech->reverseStrideLength, move_joints); case 11: case 13: if ( demand < mech->reverseSpeedMax && (cycle - cycle_rate * carryover) < mech->reverseSpeedMax ) { return mech->BodyTransition(0xe, tail_time, move_joints); } return mech->BodyTransition( 0xc, carryover * cycle * time_scale / mech->reverseStrideLength, move_joints); // // The reverse cycle (@0x4a707d and @0x4a712c). Note these are the BACK // gait, not a limp, despite sharing the gimp caps: while the demand stays // below gimpSpeedMax the cycle alternates 0x12 <-> 0x13, and a forward // demand leaves through the back-to-stand pair. Reading them as "gimp, // fall back to standing" makes the body loop stand -> reverse-entry // forever, which is a slow reverse with a wrong-footed exit. // case 16: case 18: if ( demand > mech->gimpSpeedMax && (mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax ) { return mech->BodyTransition(0x15, tail_time, move_joints); } { Scalar cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength; if (cycle_time <= 0.0f) { cycle_time = -cycle_time; } return mech->BodyTransition(0x13, cycle_time, move_joints); } case 17: case 19: if ( demand > mech->gimpSpeedMax && (mech->gimpCycleRate * carryover + cycle) > mech->gimpSpeedMax ) { return mech->BodyTransition(0x14, tail_time, move_joints); } { Scalar cycle_time = carryover * cycle * time_scale / mech->gimpStrideLength; if (cycle_time <= 0.0f) { cycle_time = -cycle_time; } return mech->BodyTransition(0x12, cycle_time, move_joints); } } return 0.0f; }