2
0
Эх сурвалжийг харах

Added new framework for saving player inputs (#796)

This allows recording/replay + determinism checks for samples where the player provides input like when driving vehicles or steering a character.
Jorrit Rouwe 1 жил өмнө
parent
commit
e594cec6b4

+ 2 - 2
Jolt/Physics/Vehicle/MotorcycleController.cpp

@@ -260,14 +260,14 @@ bool MotorcycleController::SolveLongitudinalAndLateralConstraints(float inDeltaT
 	return impulse;
 }
 
-void MotorcycleController::SaveState(StateRecorder& inStream) const
+void MotorcycleController::SaveState(StateRecorder &inStream) const
 {
 	WheeledVehicleController::SaveState(inStream);
 
 	inStream.Write(mTargetLean);
 }
 
-void MotorcycleController::RestoreState(StateRecorder& inStream)
+void MotorcycleController::RestoreState(StateRecorder &inStream)
 {
 	WheeledVehicleController::RestoreState(inStream);
 

+ 2 - 2
Jolt/Physics/Vehicle/MotorcycleController.h

@@ -87,8 +87,8 @@ protected:
 	// See: VehicleController
 	virtual void				PreCollide(float inDeltaTime, PhysicsSystem &inPhysicsSystem) override;
 	virtual bool				SolveLongitudinalAndLateralConstraints(float inDeltaTime) override;
-	virtual void				SaveState(StateRecorder& inStream) const override;
-	virtual void				RestoreState(StateRecorder& inStream) override;
+	virtual void				SaveState(StateRecorder &inStream) const override;
+	virtual void				RestoreState(StateRecorder &inStream) override;
 #ifdef JPH_DEBUG_RENDERER
 	virtual void				Draw(DebugRenderer *inRenderer) const override;
 #endif // JPH_DEBUG_RENDERER

+ 28 - 7
Samples/SamplesApp.cpp

@@ -2095,8 +2095,12 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 			ClearDebugRenderer();
 
 			// Restore state to what it was during that time
-			StateRecorderImpl &recorder = mPlaybackFrames[mCurrentPlaybackFrame];
-			RestoreState(recorder);
+			PlayBackFrame &frame = mPlaybackFrames[mCurrentPlaybackFrame];
+			RestoreState(frame.mState);
+
+			// Also restore input back to what it was at the time
+			frame.mInputState.Rewind();
+			mTest->RestoreInputState(frame.mInputState);
 
 			// Physics world is drawn using debug lines, when not paused
 			// Draw state prior to step so that debug lines are created from the same state
@@ -2114,7 +2118,7 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 
 			// Validate that update result is the same as the previously recorded state
 			if (check_determinism && mCurrentPlaybackFrame < (int)mPlaybackFrames.size() - 1)
-				ValidateState(mPlaybackFrames[mCurrentPlaybackFrame + 1]);
+				ValidateState(mPlaybackFrames[mCurrentPlaybackFrame + 1].mState);
 		}
 
 		// On the last frame go back to play mode
@@ -2138,11 +2142,24 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 			// Debugging functionality like shooting a ball and dragging objects
 			UpdateDebug(inDeltaTime);
 
+			{
+				// Pocess input, this is done once and before we save the state so that we can save the input state
+				JPH_PROFILE("ProcessInput");
+				Test::ProcessInputParams handle_input;
+				handle_input.mDeltaTime = 1.0f / mUpdateFrequency;
+				handle_input.mKeyboard = mKeyboard;
+				handle_input.mCameraState = GetCamera();
+				mTest->ProcessInput(handle_input);
+			}
+
 			if (mRecordState || check_determinism)
 			{
 				// Record the state prior to the step
-				mPlaybackFrames.push_back(StateRecorderImpl());
-				SaveState(mPlaybackFrames.back());
+				mPlaybackFrames.push_back(PlayBackFrame());
+				SaveState(mPlaybackFrames.back().mState);
+
+				// Save input too
+				mTest->SaveInputState(mPlaybackFrames.back().mInputState);
 			}
 
 			// Physics world is drawn using debug lines, when not paused
@@ -2166,7 +2183,12 @@ bool SamplesApp::RenderFrame(float inDeltaTime)
 				SaveState(post_step_state);
 
 				// Restore to the previous state
-				RestoreState(mPlaybackFrames.back());
+				PlayBackFrame &frame = mPlaybackFrames.back();
+				RestoreState(frame.mState);
+
+				// Also restore input back to what it was at the time
+				frame.mInputState.Rewind();
+				mTest->RestoreInputState(frame.mInputState);
 
 				// Step again
 				StepPhysics(mJobSystemValidating);
@@ -2342,7 +2364,6 @@ void SamplesApp::StepPhysics(JobSystem *inJobSystem)
 		JPH_PROFILE("PrePhysicsUpdate");
 		Test::PreUpdateParams pre_update;
 		pre_update.mDeltaTime = delta_time;
-		pre_update.mKeyboard = mKeyboard;
 		pre_update.mCameraState = GetCamera();
 	#ifdef JPH_DEBUG_RENDERER
 		pre_update.mPoseDrawSettings = &mPoseDrawSettings;

+ 6 - 1
Samples/SamplesApp.h

@@ -130,7 +130,12 @@ private:
 	// State recording and determinism checks
 	bool					mRecordState = false;										// When true, the state of the physics system is recorded in mPlaybackFrames every physics update
 	bool					mCheckDeterminism = false;									// When true, the physics state is rolled back after every update and run again to verify that the state is the same
-	Array<StateRecorderImpl> mPlaybackFrames;											// A list of recorded world states, one per physics simulation step
+	struct PlayBackFrame
+	{
+		StateRecorderImpl	mInputState;												// State of the player inputs at the beginning of the step
+		StateRecorderImpl	mState;														// Main simulation state
+	};
+	Array<PlayBackFrame>	mPlaybackFrames;											// A list of recorded world states, one per physics simulation step
 	enum class EPlaybackMode
 	{
 		Rewind,

+ 34 - 17
Samples/Tests/Character/CharacterBaseTest.cpp

@@ -529,37 +529,40 @@ void CharacterBaseTest::Initialize()
 	}
 }
 
-void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void CharacterBaseTest::ProcessInput(const ProcessInputParams &inParams)
 {
-	// Update scene time
-	mTime += inParams.mDeltaTime;
-
 	// Determine controller input
-	Vec3 control_input = Vec3::sZero();
-	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		control_input.SetZ(-1);
-	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	control_input.SetZ(1);
-	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		control_input.SetX(1);
-	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		control_input.SetX(-1);
-	if (control_input != Vec3::sZero())
-		control_input = control_input.Normalized();
+	mControlInput = Vec3::sZero();
+	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		mControlInput.SetZ(-1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	mControlInput.SetZ(1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		mControlInput.SetX(1);
+	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		mControlInput.SetX(-1);
+	if (mControlInput != Vec3::sZero())
+		mControlInput = mControlInput.Normalized();
 
 	// Rotate controls to align with the camera
 	Vec3 cam_fwd = inParams.mCameraState.mForward;
 	cam_fwd.SetY(0.0f);
 	cam_fwd = cam_fwd.NormalizedOr(Vec3::sAxisX());
 	Quat rotation = Quat::sFromTo(Vec3::sAxisX(), cam_fwd);
-	control_input = rotation * control_input;
+	mControlInput = rotation * mControlInput;
 
 	// Check actions
-	bool jump = false;
-	bool switch_stance = false;
+	mJump = false;
+	mSwitchStance = false;
 	for (int key = inParams.mKeyboard->GetFirstKey(); key != 0; key = inParams.mKeyboard->GetNextKey())
 	{
 		if (key == DIK_RSHIFT)
-			switch_stance = true;
+			mSwitchStance = true;
 		else if (key == DIK_RCONTROL)
-			jump = true;
+			mJump = true;
 	}
+}
+
+void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	// Update scene time
+	mTime += inParams.mDeltaTime;
 
 	// Animate bodies
 	if (!mRotatingBody.IsInvalid())
@@ -595,7 +598,7 @@ void CharacterBaseTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	}
 
 	// Call handle input after new velocities have been set to avoid frame delay
-	HandleInput(control_input, jump, switch_stance, inParams.mDeltaTime);
+	HandleInput(mControlInput, mJump, mSwitchStance, inParams.mDeltaTime);
 }
 
 void CharacterBaseTest::CreateSettingsMenu(DebugUI *inUI, UIElement *inSubMenu)
@@ -655,6 +658,20 @@ void CharacterBaseTest::RestoreState(StateRecorder &inStream)
 	inStream.Read(mReversingVerticallyMovingVelocity);
 }
 
+void CharacterBaseTest::SaveInputState(StateRecorder &inStream) const
+{
+	inStream.Write(mControlInput);
+	inStream.Write(mJump);
+	inStream.Write(mSwitchStance);
+}
+
+void CharacterBaseTest::RestoreInputState(StateRecorder &inStream)
+{
+	inStream.Read(mControlInput);
+	inStream.Read(mJump);
+	inStream.Read(mSwitchStance);
+}
+
 void CharacterBaseTest::DrawCharacterState(const CharacterBase *inCharacter, RMat44Arg inCharacterTransform, Vec3Arg inCharacterVelocity)
 {
 	// Draw current location

+ 12 - 0
Samples/Tests/Character/CharacterBaseTest.h

@@ -19,6 +19,9 @@ public:
 	// Initialize the test
 	virtual void			Initialize() override;
 
+	// Process input
+	virtual void			ProcessInput(const ProcessInputParams &inParams) override;
+
 	// Update the test, called before the physics update
 	virtual void			PrePhysicsUpdate(const PreUpdateParams &inParams) override;
 
@@ -36,6 +39,10 @@ public:
 	virtual void			SaveState(StateRecorder &inStream) const override;
 	virtual void			RestoreState(StateRecorder &inStream) override;
 
+	// Saving / restoring controller input state for replay
+	virtual void			SaveInputState(StateRecorder &inStream) const override;
+	virtual void			RestoreInputState(StateRecorder &inStream) override;
+
 protected:
 	// Get position of the character
 	virtual RVec3			GetCharacterPosition() const = 0;
@@ -106,4 +113,9 @@ private:
 	BodyID					mReversingVerticallyMovingBody;
 	float					mReversingVerticallyMovingVelocity = 1.0f;
 	BodyID					mHorizontallyMovingBody;
+
+	// Player input
+	Vec3					mControlInput = Vec3::sZero();
+	bool					mJump = false;
+	bool					mSwitchStance = false;
 };

+ 38 - 26
Samples/Tests/Character/CharacterSpaceShipTest.cpp

@@ -44,25 +44,8 @@ void CharacterSpaceShipTest::Initialize()
 	mSpaceShipPrevTransform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
 }
 
-void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void CharacterSpaceShipTest::ProcessInput(const ProcessInputParams &inParams)
 {
-	// Update scene time
-	mTime += inParams.mDeltaTime;
-
-	// Update the character so it stays relative to the space ship
-	RMat44 new_space_ship_transform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
-	mCharacter->SetPosition(new_space_ship_transform * mSpaceShipPrevTransform.Inversed() * mCharacter->GetPosition());
-
-	// Update the character rotation and its up vector to match the new up vector of the ship
-	mCharacter->SetUp(new_space_ship_transform.GetAxisY());
-	mCharacter->SetRotation(new_space_ship_transform.GetRotation().GetQuaternion());
-
-	// Draw character pre update (the sim is also drawn pre update)
-	// Note that we have first updated the position so that it matches the new position of the ship
-#ifdef JPH_DEBUG_RENDERER
-	mCharacter->GetShape()->Draw(mDebugRenderer, mCharacter->GetCenterOfMassTransform(), Vec3::sReplicate(1.0f), Color::sGreen, false, true);
-#endif // JPH_DEBUG_RENDERER
-
 	// Determine controller input
 	Vec3 control_input = Vec3::sZero();
 	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		control_input.SetZ(-1);
@@ -73,6 +56,7 @@ void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		control_input = control_input.Normalized();
 
 	// Calculate the desired velocity in local space to the ship based on the camera forward
+	RMat44 new_space_ship_transform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
 	Vec3 cam_fwd = new_space_ship_transform.GetRotation().Multiply3x3Transposed(inParams.mCameraState.mForward);
 	cam_fwd.SetY(0.0f);
 	cam_fwd = cam_fwd.NormalizedOr(Vec3::sAxisX());
@@ -82,13 +66,31 @@ void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	// Smooth the player input in local space to the ship
 	mDesiredVelocity = 0.25f * control_input * cCharacterSpeed + 0.75f * mDesiredVelocity;
 
-	// Check jump
-	bool jump = false;
+	// Check actions
+	mJump = false;
 	for (int key = inParams.mKeyboard->GetFirstKey(); key != 0; key = inParams.mKeyboard->GetNextKey())
-	{
 		if (key == DIK_RCONTROL)
-			jump = true;
-	}
+			mJump = true;
+}
+
+void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	// Update scene time
+	mTime += inParams.mDeltaTime;
+
+	// Update the character so it stays relative to the space ship
+	RMat44 new_space_ship_transform = mBodyInterface->GetCenterOfMassTransform(mSpaceShip);
+	mCharacter->SetPosition(new_space_ship_transform * mSpaceShipPrevTransform.Inversed() * mCharacter->GetPosition());
+
+	// Update the character rotation and its up vector to match the new up vector of the ship
+	mCharacter->SetUp(new_space_ship_transform.GetAxisY());
+	mCharacter->SetRotation(new_space_ship_transform.GetRotation().GetQuaternion());
+
+	// Draw character pre update (the sim is also drawn pre update)
+	// Note that we have first updated the position so that it matches the new position of the ship
+#ifdef JPH_DEBUG_RENDERER
+	mCharacter->GetShape()->Draw(mDebugRenderer, mCharacter->GetCenterOfMassTransform(), Vec3::sReplicate(1.0f), Color::sGreen, false, true);
+#endif // JPH_DEBUG_RENDERER
 
 	// Determine new character velocity
 	Vec3 current_vertical_velocity = mCharacter->GetLinearVelocity().Dot(mSpaceShipPrevTransform.GetAxisY()) * mCharacter->GetUp();
@@ -101,7 +103,7 @@ void CharacterSpaceShipTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		new_velocity = ground_velocity;
 
 		// Jump
-		if (jump)
+		if (mJump)
 			new_velocity += cJumpSpeed * mCharacter->GetUp();
 	}
 	else
@@ -163,7 +165,6 @@ void CharacterSpaceShipTest::SaveState(StateRecorder &inStream) const
 	mCharacter->SaveState(inStream);
 
 	inStream.Write(mTime);
-	inStream.Write(mDesiredVelocity);
 	inStream.Write(mSpaceShipPrevTransform);
 }
 
@@ -172,13 +173,24 @@ void CharacterSpaceShipTest::RestoreState(StateRecorder &inStream)
 	mCharacter->RestoreState(inStream);
 
 	inStream.Read(mTime);
-	inStream.Read(mDesiredVelocity);
 	inStream.Read(mSpaceShipPrevTransform);
 
 	// Calculate new velocity
 	UpdateShipVelocity();
 }
 
+void CharacterSpaceShipTest::SaveInputState(StateRecorder &inStream) const
+{
+	inStream.Write(mDesiredVelocity);
+	inStream.Write(mJump);
+}
+
+void CharacterSpaceShipTest::RestoreInputState(StateRecorder &inStream)
+{
+	inStream.Read(mDesiredVelocity);
+	inStream.Read(mJump);
+}
+
 void CharacterSpaceShipTest::OnAdjustBodyVelocity(const CharacterVirtual *inCharacter, const Body &inBody2, Vec3 &ioLinearVelocity, Vec3 &ioAngularVelocity)
 {
 	// Cancel out velocity of space ship, we move relative to this which means we don't feel any of the acceleration of the ship (= engage inertial dampeners!)

+ 9 - 1
Samples/Tests/Character/CharacterSpaceShipTest.h

@@ -18,6 +18,9 @@ public:
 	// Initialize the test
 	virtual void			Initialize() override;
 
+	// Process input
+	virtual void			ProcessInput(const ProcessInputParams &inParams) override;
+
 	// Update the test, called before the physics update
 	virtual void			PrePhysicsUpdate(const PreUpdateParams &inParams) override;
 
@@ -31,6 +34,10 @@ public:
 	virtual void			SaveState(StateRecorder &inStream) const override;
 	virtual void			RestoreState(StateRecorder &inStream) override;
 
+	// Saving / restoring controller input state for replay
+	virtual void			SaveInputState(StateRecorder &inStream) const override;
+	virtual void			RestoreInputState(StateRecorder &inStream) override;
+
 private:
 	// Calculate new ship velocity
 	void					UpdateShipVelocity();
@@ -60,6 +67,7 @@ private:
 	// Global time
 	float					mTime = 0.0f;
 
-	// Smoothed value of the player input
+	// Player input
 	Vec3					mDesiredVelocity = Vec3::sZero();
+	bool					mJump = false;
 };

+ 4 - 1
Samples/Tests/ConvexCollision/InteractivePairsTest.cpp

@@ -17,7 +17,7 @@ JPH_IMPLEMENT_RTTI_VIRTUAL(InteractivePairsTest)
 	JPH_ADD_BASE_CLASS(InteractivePairsTest, Test)
 }
 
-void InteractivePairsTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void InteractivePairsTest::ProcessInput(const ProcessInputParams &inParams)
 {
 	// Keyboard controls
 	if (inParams.mKeyboard->IsKeyPressed(DIK_Z))
@@ -44,7 +44,10 @@ void InteractivePairsTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		mDistance = 4.0f;
 	if (mDistance > 4.0f)
 		mDistance = -4.0f;
+}
 
+void InteractivePairsTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
 	float z = 0.0f;
 
 	const float r1 = 0.25f * JPH_PI;

+ 3 - 0
Samples/Tests/ConvexCollision/InteractivePairsTest.h

@@ -12,6 +12,9 @@ class InteractivePairsTest : public Test
 public:
 	JPH_DECLARE_RTTI_VIRTUAL(JPH_NO_EXPORT, InteractivePairsTest)
 
+	// Process input
+	virtual void	ProcessInput(const ProcessInputParams &inParams) override;
+
 	// Update the test, called before the physics update
 	virtual void	PrePhysicsUpdate(const PreUpdateParams &inParams) override;
 

+ 16 - 1
Samples/Tests/Test.h

@@ -48,12 +48,23 @@ public:
 	// If this test implements a contact listener, it should be returned here
 	virtual ContactListener *GetContactListener()								{ return nullptr; }
 
-	class PreUpdateParams
+	class ProcessInputParams
 	{
 	public:
 		float								mDeltaTime;
 		Keyboard *							mKeyboard;
 		CameraState							mCameraState;
+	};
+
+	// Process input, this is called before SaveInputState is called. This allows you to determine the player input and adjust internal state accordingly.
+	// This state should not be applied until PrePhysicsUpdate because on replay you will receive a call to RestoreInputState to restore the stored player input state before receiging another PrePhysicsUpdate.
+	virtual void	ProcessInput(const ProcessInputParams &inParams)			{ }
+
+	class PreUpdateParams
+	{
+	public:
+		float								mDeltaTime;
+		CameraState							mCameraState;
 #ifdef JPH_DEBUG_RENDERER
 		const SkeletonPose::DrawSettings *	mPoseDrawSettings;
 #endif // JPH_DEBUG_RENDERER
@@ -89,6 +100,10 @@ public:
 	virtual void	SaveState(StateRecorder &inStream) const					{ }
 	virtual void	RestoreState(StateRecorder &inStream)						{ }
 
+	// Saving / restoring controller input state for replay
+	virtual void	SaveInputState(StateRecorder &inStream) const				{ }
+	virtual void	RestoreInputState(StateRecorder &inStream)					{ }
+
 	// Return a string that is displayed in the top left corner of the screen
 	virtual String	GetStatusString() const										{ return String(); }
 

+ 34 - 28
Samples/Tests/Vehicle/MotorcycleTest.cpp

@@ -127,66 +127,71 @@ void MotorcycleTest::Initialize()
 	mPhysicsSystem->AddStepListener(mVehicleConstraint);
 }
 
-void MotorcycleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void MotorcycleTest::ProcessInput(const ProcessInputParams &inParams)
 {
-	VehicleTest::PrePhysicsUpdate(inParams);
-
 	// Determine acceleration and brake
-	float forward = 0.0f, right = 0.0f, brake = 0.0f;
+	mForward = 0.0f;
+	mBrake = 0.0f;
 	if (inParams.mKeyboard->IsKeyPressed(DIK_Z))
-		brake = 1.0f;
+		mBrake = 1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_UP))
-		forward = 1.0f;
+		mForward = 1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))
-		forward = -1.0f;
+		mForward = -1.0f;
 
 	// Check if we're reversing direction
-	if (mPreviousForward * forward < 0.0f)
+	if (mPreviousForward * mForward < 0.0f)
 	{
 		// Get vehicle velocity in local space to the body of the vehicle
 		float velocity = (mMotorcycleBody->GetRotation().Conjugated() * mMotorcycleBody->GetLinearVelocity()).GetZ();
-		if ((forward > 0.0f && velocity < -0.1f) || (forward < 0.0f && velocity > 0.1f))
+		if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
 		{
 			// Brake while we've not stopped yet
-			forward = 0.0f;
-			brake = 1.0f;
+			mForward = 0.0f;
+			mBrake = 1.0f;
 		}
 		else
 		{
 			// When we've come to a stop, accept the new direction
-			mPreviousForward = forward;
+			mPreviousForward = mForward;
 		}
 	}
 
 	// Steering
+	float right = 0.0f;
 	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))
 		right = -1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))
 		right = 1.0f;
 	const float steer_speed = 4.0f;
-	if (right > mCurrentRight)
-		mCurrentRight = min(mCurrentRight + steer_speed * inParams.mDeltaTime, right);
-	else if (right < mCurrentRight)
-		mCurrentRight = max(mCurrentRight - steer_speed * inParams.mDeltaTime, right);
+	if (right > mRight)
+		mRight = min(mRight + steer_speed * inParams.mDeltaTime, right);
+	else if (right < mRight)
+		mRight = max(mRight - steer_speed * inParams.mDeltaTime, right);
 
 	// When leaned, we don't want to use the brakes fully as we'll spin out
-	if (brake > 0.0f)
+	if (mBrake > 0.0f)
 	{
 		Vec3 world_up = -mPhysicsSystem->GetGravity().Normalized();
 		Vec3 up = mMotorcycleBody->GetRotation() * mVehicleConstraint->GetLocalUp();
 		Vec3 fwd = mMotorcycleBody->GetRotation() * mVehicleConstraint->GetLocalForward();
 		float sin_lean_angle = abs(world_up.Cross(up).Dot(fwd));
 		float brake_multiplier = Square(1.0f - sin_lean_angle);
-		brake *= brake_multiplier;
+		mBrake *= brake_multiplier;
 	}
+}
+
+void MotorcycleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	VehicleTest::PrePhysicsUpdate(inParams);
 
 	// On user input, assure that the motorcycle is active
-	if (mCurrentRight != 0.0f || forward != 0.0f || brake != 0.0f)
+	if (mRight != 0.0f || mForward != 0.0f || mBrake != 0.0f)
 		mBodyInterface->ActivateBody(mMotorcycleBody->GetID());
 
 	// Pass the input on to the constraint
 	MotorcycleController *controller = static_cast<MotorcycleController *>(mVehicleConstraint->GetController());
-	controller->SetDriverInput(forward, mCurrentRight, brake, false);
+	controller->SetDriverInput(mForward, mRight, mBrake, false);
 	controller->EnableLeanController(sEnableLeanController);
 
 	// Draw our wheels (this needs to be done in the pre update since we draw the bodies too in the state before the step)
@@ -198,20 +203,21 @@ void MotorcycleTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	}
 }
 
-void MotorcycleTest::SaveState(StateRecorder& inStream) const
-{
-	VehicleTest::SaveState(inStream);
 
+void MotorcycleTest::SaveInputState(StateRecorder &inStream) const
+{
+	inStream.Write(mForward);
 	inStream.Write(mPreviousForward);
-	inStream.Write(mCurrentRight);
+	inStream.Write(mRight);
+	inStream.Write(mBrake);
 }
 
-void MotorcycleTest::RestoreState(StateRecorder& inStream)
+void MotorcycleTest::RestoreInputState(StateRecorder &inStream)
 {
-	VehicleTest::RestoreState(inStream);
-
+	inStream.Read(mForward);
 	inStream.Read(mPreviousForward);
-	inStream.Read(mCurrentRight);
+	inStream.Read(mRight);
+	inStream.Read(mBrake);
 }
 
 void MotorcycleTest::GetInitialCamera(CameraState &ioState) const

+ 8 - 3
Samples/Tests/Vehicle/MotorcycleTest.h

@@ -19,9 +19,10 @@ public:
 
 	// See: Test
 	virtual void				Initialize() override;
+	virtual void				ProcessInput(const ProcessInputParams &inParams) override;
 	virtual void				PrePhysicsUpdate(const PreUpdateParams &inParams) override;
-	virtual void				SaveState(StateRecorder& inStream) const override;
-	virtual void				RestoreState(StateRecorder& inStream) override;
+	virtual void				SaveInputState(StateRecorder &inStream) const override;
+	virtual void				RestoreInputState(StateRecorder &inStream) override;
 
 	virtual void				GetInitialCamera(CameraState &ioState) const override;
 	virtual RMat44				GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
@@ -35,6 +36,10 @@ private:
 
 	Body *						mMotorcycleBody;							///< The vehicle
 	Ref<VehicleConstraint>		mVehicleConstraint;							///< The vehicle constraint
+
+	// Player input
+	float						mForward = 0.0f;
 	float						mPreviousForward = 1.0f;					///< Keeps track of last motorcycle direction so we know when to brake and when to accelerate
-	float						mCurrentRight = 0.0f;						///< Keeps track of the current steering angle (radians)
+	float						mRight = 0.0f;								///< Keeps track of the current steering angle
+	float						mBrake = 0.0f;
 };

+ 72 - 41
Samples/Tests/Vehicle/TankTest.cpp

@@ -162,75 +162,64 @@ void TankTest::Initialize()
 	mPhysicsSystem->AddConstraint(mBarrelHinge);
 }
 
-void TankTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void TankTest::ProcessInput(const ProcessInputParams &inParams)
 {
-	VehicleTest::PrePhysicsUpdate(inParams);
-
 	const float min_velocity_pivot_turn = 1.0f;
 
-	const float bullet_radius = 0.061f; // 120 mm
-	const Vec3 bullet_pos = Vec3(0, 1.6f, 0);
-	const Vec3 bullet_velocity = Vec3(0, 400.0f, 0); // Normal exit velocities are around 1100-1700 m/s, use a lower variable as we have a limit to max velocity (See: https://tanks-encyclopedia.com/coldwar-usa-120mm-gun-tank-m1e1-abrams/)
-	const float bullet_mass = 40.0f; // Normal projectile weight is around 7 kg, use an increased value so the momentum is more realistic (with the lower exit velocity)
-	const float bullet_reload_time = 2.0f;
-
 	// Determine acceleration and brake
-	float forward = 0.0f, left_ratio = 1.0f, right_ratio = 1.0f, brake = 0.0f;
+	mForward = 0.0f;
+	mBrake = 0.0f;
 	if (inParams.mKeyboard->IsKeyPressed(DIK_RSHIFT))
-		brake = 1.0f;
+		mBrake = 1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_UP))
-		forward = 1.0f;
+		mForward = 1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))
-		forward = -1.0f;
+		mForward = -1.0f;
 
 	// Steering
+	mLeftRatio = 1.0f;
+	mRightRatio = 1.0f;
 	float velocity = (mTankBody->GetRotation().Conjugated() * mTankBody->GetLinearVelocity()).GetZ();
 	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))
 	{
-		if (brake == 0.0f && forward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
+		if (mBrake == 0.0f && mForward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
 		{
 			// Pivot turn
-			left_ratio = -1.0f;
-			forward = 1.0f;
+			mLeftRatio = -1.0f;
+			mForward = 1.0f;
 		}
 		else
-			left_ratio = 0.6f;
+			mLeftRatio = 0.6f;
 	}
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))
 	{
-		if (brake == 0.0f && forward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
+		if (mBrake == 0.0f && mForward == 0.0f && abs(velocity) < min_velocity_pivot_turn)
 		{
 			// Pivot turn
-			right_ratio = -1.0f;
-			forward = 1.0f;
+			mRightRatio = -1.0f;
+			mForward = 1.0f;
 		}
 		else
-			right_ratio = 0.6f;
+			mRightRatio = 0.6f;
 	}
 
 	// Check if we're reversing direction
-	if (mPreviousForward * forward < 0.0f)
+	if (mPreviousForward * mForward < 0.0f)
 	{
 		// Get vehicle velocity in local space to the body of the vehicle
-		if ((forward > 0.0f && velocity < -0.1f) || (forward < 0.0f && velocity > 0.1f))
+		if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
 		{
 			// Brake while we've not stopped yet
-			forward = 0.0f;
-			brake = 1.0f;
+			mForward = 0.0f;
+			mBrake = 1.0f;
 		}
 		else
 		{
 			// When we've come to a stop, accept the new direction
-			mPreviousForward = forward;
+			mPreviousForward = mForward;
 		}
 	}
 
-	// Assure the tank stays active as we're controlling the turret with the mouse
-	mBodyInterface->ActivateBody(mTankBody->GetID());
-
-	// Pass the input on to the constraint
-	static_cast<TrackedVehicleController *>(mVehicleConstraint->GetController())->SetDriverInput(forward, left_ratio, right_ratio, brake);
-
 	// Cast ray to find target
 	RRayCast ray { inParams.mCameraState.mPos, 1000.0f * inParams.mCameraState.mForward };
 	RayCastSettings ray_settings;
@@ -247,20 +236,40 @@ void TankTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	// Orient the turret towards the hit position
 	RMat44 turret_to_world = mTankBody->GetCenterOfMassTransform() * mTurretHinge->GetConstraintToBody1Matrix();
 	Vec3 hit_pos_in_turret = Vec3(turret_to_world.InversedRotationTranslation() * hit_pos);
-	float heading = ATan2(hit_pos_in_turret.GetZ(), hit_pos_in_turret.GetY());
-	mTurretHinge->SetTargetAngle(heading);
+	mTurretHeading = ATan2(hit_pos_in_turret.GetZ(), hit_pos_in_turret.GetY());
 
 	// Orient barrel towards the hit position
 	RMat44 barrel_to_world = mTurretBody->GetCenterOfMassTransform() * mBarrelHinge->GetConstraintToBody1Matrix();
 	Vec3 hit_pos_in_barrel = Vec3(barrel_to_world.InversedRotationTranslation() * hit_pos);
-	float pitch = ATan2(hit_pos_in_barrel.GetZ(), hit_pos_in_barrel.GetY());
-	mBarrelHinge->SetTargetAngle(pitch);
+	mBarrelPitch = ATan2(hit_pos_in_barrel.GetZ(), hit_pos_in_barrel.GetY());
+
+	// If user wants to fire
+	mFire = inParams.mKeyboard->IsKeyPressed(DIK_RETURN);
+}
+
+void TankTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	VehicleTest::PrePhysicsUpdate(inParams);
+
+	const float bullet_radius = 0.061f; // 120 mm
+	const Vec3 bullet_pos = Vec3(0, 1.6f, 0);
+	const Vec3 bullet_velocity = Vec3(0, 400.0f, 0); // Normal exit velocities are around 1100-1700 m/s, use a lower variable as we have a limit to max velocity (See: https://tanks-encyclopedia.com/coldwar-usa-120mm-gun-tank-m1e1-abrams/)
+	const float bullet_mass = 40.0f; // Normal projectile weight is around 7 kg, use an increased value so the momentum is more realistic (with the lower exit velocity)
+	const float bullet_reload_time = 2.0f;
+
+	// Assure the tank stays active as we're controlling the turret with the mouse
+	mBodyInterface->ActivateBody(mTankBody->GetID());
+
+	// Pass the input on to the constraint
+	static_cast<TrackedVehicleController *>(mVehicleConstraint->GetController())->SetDriverInput(mForward, mLeftRatio, mRightRatio, mBrake);
+	mTurretHinge->SetTargetAngle(mTurretHeading);
+	mBarrelHinge->SetTargetAngle(mBarrelPitch);
 
 	// Update reload time
 	mReloadTime = max(0.0f, mReloadTime - inParams.mDeltaTime);
 
 	// Shoot bullet
-	if (mReloadTime == 0.0f && inParams.mKeyboard->IsKeyPressed(DIK_RETURN))
+	if (mReloadTime == 0.0f && mFire)
 	{
 		// Create bullet
 		BodyCreationSettings bullet_creation_settings(new SphereShape(bullet_radius), mBarrelBody->GetCenterOfMassTransform() * bullet_pos, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
@@ -289,22 +298,44 @@ void TankTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	}
 }
 
-void TankTest::SaveState(StateRecorder& inStream) const
+void TankTest::SaveState(StateRecorder &inStream) const
 {
 	VehicleTest::SaveState(inStream);
 
-	inStream.Write(mPreviousForward);
 	inStream.Write(mReloadTime);
 }
 
-void TankTest::RestoreState(StateRecorder& inStream)
+void TankTest::RestoreState(StateRecorder &inStream)
 {
 	VehicleTest::RestoreState(inStream);
 
-	inStream.Read(mPreviousForward);
 	inStream.Read(mReloadTime);
 }
 
+void TankTest::SaveInputState(StateRecorder &inStream) const
+{
+	inStream.Write(mForward);
+	inStream.Write(mPreviousForward);
+	inStream.Write(mLeftRatio);
+	inStream.Write(mRightRatio);
+	inStream.Write(mBrake);
+	inStream.Write(mTurretHeading);
+	inStream.Write(mBarrelPitch);
+	inStream.Write(mFire);
+}
+
+void TankTest::RestoreInputState(StateRecorder &inStream)
+{
+	inStream.Read(mForward);
+	inStream.Read(mPreviousForward);
+	inStream.Read(mLeftRatio);
+	inStream.Read(mRightRatio);
+	inStream.Read(mBrake);
+	inStream.Read(mTurretHeading);
+	inStream.Read(mBarrelPitch);
+	inStream.Read(mFire);
+}
+
 void TankTest::GetInitialCamera(CameraState &ioState) const
 {
 	// Position camera behind tank

+ 15 - 3
Samples/Tests/Vehicle/TankTest.h

@@ -19,9 +19,12 @@ public:
 
 	// See: Test
 	virtual void				Initialize() override;
+	virtual void				ProcessInput(const ProcessInputParams &inParams) override;
 	virtual void				PrePhysicsUpdate(const PreUpdateParams &inParams) override;
-	virtual void				SaveState(StateRecorder& inStream) const override;
-	virtual void				RestoreState(StateRecorder& inStream) override;
+	virtual void				SaveState(StateRecorder &inStream) const override;
+	virtual void				RestoreState(StateRecorder &inStream) override;
+	virtual void				SaveInputState(StateRecorder &inStream) const override;
+	virtual void				RestoreInputState(StateRecorder &inStream) override;
 
 	virtual void				GetInitialCamera(CameraState &ioState) const override;
 	virtual RMat44				GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
@@ -33,6 +36,15 @@ private:
 	Ref<VehicleConstraint>		mVehicleConstraint;							///< The vehicle constraint
 	Ref<HingeConstraint>		mTurretHinge;								///< Hinge connecting tank body and turret
 	Ref<HingeConstraint>		mBarrelHinge;								///< Hinge connecting tank turret and barrel
-	float						mPreviousForward = 1.0f;					///< Keeps track of last car direction so we know when to brake and when to accelerate
 	float						mReloadTime = 0.0f;							///< How long it still takes to reload the main gun
+
+	// Player input
+	float						mForward = 0.0f;
+	float						mPreviousForward = 1.0f;					///< Keeps track of last car direction so we know when to brake and when to accelerate
+	float						mLeftRatio = 0.0f;
+	float						mRightRatio = 0.0f;
+	float						mBrake = 0.0f;
+	float						mTurretHeading = 0.0f;
+	float						mBarrelPitch = 0.0f;
+	bool						mFire = false;
 };

+ 33 - 23
Samples/Tests/Vehicle/VehicleConstraintTest.cpp

@@ -156,50 +156,56 @@ void VehicleConstraintTest::Initialize()
 	mPhysicsSystem->AddStepListener(mVehicleConstraint);
 }
 
-void VehicleConstraintTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void VehicleConstraintTest::ProcessInput(const ProcessInputParams &inParams)
 {
-	VehicleTest::PrePhysicsUpdate(inParams);
-
 	// Determine acceleration and brake
-	float forward = 0.0f, right = 0.0f, brake = 0.0f, hand_brake = 0.0f;
+	mForward = 0.0f;
 	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))
-		forward = 1.0f;
+		mForward = 1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))
-		forward = -1.0f;
+		mForward = -1.0f;
 
 	// Check if we're reversing direction
-	if (mPreviousForward * forward < 0.0f)
+	mBrake = 0.0f;
+	if (mPreviousForward * mForward < 0.0f)
 	{
 		// Get vehicle velocity in local space to the body of the vehicle
 		float velocity = (mCarBody->GetRotation().Conjugated() * mCarBody->GetLinearVelocity()).GetZ();
-		if ((forward > 0.0f && velocity < -0.1f) || (forward < 0.0f && velocity > 0.1f))
+		if ((mForward > 0.0f && velocity < -0.1f) || (mForward < 0.0f && velocity > 0.1f))
 		{
 			// Brake while we've not stopped yet
-			forward = 0.0f;
-			brake = 1.0f;
+			mForward = 0.0f;
+			mBrake = 1.0f;
 		}
 		else
 		{
 			// When we've come to a stop, accept the new direction
-			mPreviousForward = forward;
+			mPreviousForward = mForward;
 		}
 	}
 
 	// Hand brake will cancel gas pedal
+	mHandBrake = 0.0f;
 	if (inParams.mKeyboard->IsKeyPressed(DIK_Z))
 	{
-		forward = 0.0f;
-		hand_brake = 1.0f;
+		mForward = 0.0f;
+		mHandBrake = 1.0f;
 	}
 
 	// Steering
+	mRight = 0.0f;
 	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))
-		right = -1.0f;
+		mRight = -1.0f;
 	else if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))
-		right = 1.0f;
+		mRight = 1.0f;
+}
+
+void VehicleConstraintTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	VehicleTest::PrePhysicsUpdate(inParams);
 
 	// On user input, assure that the car is active
-	if (right != 0.0f || forward != 0.0f || brake != 0.0f || hand_brake != 0.0f)
+	if (mRight != 0.0f || mForward != 0.0f || mBrake != 0.0f || mHandBrake != 0.0f)
 		mBodyInterface->ActivateBody(mCarBody->GetID());
 
 	WheeledVehicleController *controller = static_cast<WheeledVehicleController *>(mVehicleConstraint->GetController());
@@ -215,7 +221,7 @@ void VehicleConstraintTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 		d.mLimitedSlipRatio = limited_slip_ratio;
 
 	// Pass the input on to the constraint
-	controller->SetDriverInput(forward, right, brake, hand_brake);
+	controller->SetDriverInput(mForward, mRight, mBrake, mHandBrake);
 
 	// Set the collision tester
 	mVehicleConstraint->SetVehicleCollisionTester(mTesters[sCollisionMode]);
@@ -229,18 +235,22 @@ void VehicleConstraintTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 	}
 }
 
-void VehicleConstraintTest::SaveState(StateRecorder& inStream) const
+void VehicleConstraintTest::SaveInputState(StateRecorder &inStream) const
 {
-	VehicleTest::SaveState(inStream);
-
+	inStream.Write(mForward);
 	inStream.Write(mPreviousForward);
+	inStream.Write(mRight);
+	inStream.Write(mBrake);
+	inStream.Write(mHandBrake);
 }
 
-void VehicleConstraintTest::RestoreState(StateRecorder& inStream)
+void VehicleConstraintTest::RestoreInputState(StateRecorder &inStream)
 {
-	VehicleTest::RestoreState(inStream);
-
+	inStream.Read(mForward);
 	inStream.Read(mPreviousForward);
+	inStream.Read(mRight);
+	inStream.Read(mBrake);
+	inStream.Read(mHandBrake);
 }
 
 void VehicleConstraintTest::GetInitialCamera(CameraState &ioState) const

+ 9 - 2
Samples/Tests/Vehicle/VehicleConstraintTest.h

@@ -18,9 +18,10 @@ public:
 
 	// See: Test
 	virtual void				Initialize() override;
+	virtual void				ProcessInput(const ProcessInputParams &inParams) override;
 	virtual void				PrePhysicsUpdate(const PreUpdateParams &inParams) override;
-	virtual void				SaveState(StateRecorder& inStream) const override;
-	virtual void				RestoreState(StateRecorder& inStream) override;
+	virtual void				SaveInputState(StateRecorder &inStream) const override;
+	virtual void				RestoreInputState(StateRecorder &inStream) override;
 
 	virtual void				GetInitialCamera(CameraState &ioState) const override;
 	virtual RMat44				GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
@@ -61,5 +62,11 @@ private:
 	Body *						mCarBody;									///< The vehicle
 	Ref<VehicleConstraint>		mVehicleConstraint;							///< The vehicle constraint
 	Ref<VehicleCollisionTester>	mTesters[3];								///< Collision testers for the wheel
+
+	// Player input
+	float						mForward = 0.0f;
 	float						mPreviousForward = 1.0f;					///< Keeps track of last car direction so we know when to brake and when to accelerate
+	float						mRight = 0.0f;
+	float						mBrake = 0.0f;
+	float						mHandBrake = 0.0f;
 };

+ 29 - 13
Samples/Tests/Vehicle/VehicleSixDOFTest.cpp

@@ -118,26 +118,30 @@ void VehicleSixDOFTest::Initialize()
 	}
 }
 
-void VehicleSixDOFTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+void VehicleSixDOFTest::ProcessInput(const ProcessInputParams &inParams)
 {
-	VehicleTest::PrePhysicsUpdate(inParams);
-
 	const float max_rotation_speed = 10.0f * JPH_PI;
 
 	// Determine steering and speed
-	float steering_angle = 0.0f, speed = 0.0f;
-	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		steering_angle = cMaxSteeringAngle;
-	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	steering_angle = -cMaxSteeringAngle;
-	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		speed = max_rotation_speed;
-	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		speed = -max_rotation_speed;
+	mSteeringAngle = 0.0f;
+	mSpeed = 0.0f;
+	if (inParams.mKeyboard->IsKeyPressed(DIK_LEFT))		mSteeringAngle = cMaxSteeringAngle;
+	if (inParams.mKeyboard->IsKeyPressed(DIK_RIGHT))	mSteeringAngle = -cMaxSteeringAngle;
+	if (inParams.mKeyboard->IsKeyPressed(DIK_UP))		mSpeed = max_rotation_speed;
+	if (inParams.mKeyboard->IsKeyPressed(DIK_DOWN))		mSpeed = -max_rotation_speed;
+}
+
+void VehicleSixDOFTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
+{
+	VehicleTest::PrePhysicsUpdate(inParams);
 
 	// On user input, assure that the car is active
-	if (steering_angle != 0.0f || speed != 0.0f)
+	if (mSteeringAngle != 0.0f || mSpeed != 0.0f)
 		mBodyInterface->ActivateBody(mCarBody->GetID());
 
 	// Brake if current velocity is in the opposite direction of the desired velocity
 	float car_speed = mCarBody->GetLinearVelocity().Dot(mCarBody->GetRotation().RotateAxisZ());
-	bool brake = speed != 0.0f && car_speed != 0.0f && Sign(speed) != Sign(car_speed);
+	bool brake = mSpeed != 0.0f && car_speed != 0.0f && Sign(mSpeed) != Sign(car_speed);
 
 	// Front wheels
 	const EWheel front_wheels[] = { EWheel::LeftFront, EWheel::RightFront };
@@ -148,7 +152,7 @@ void VehicleSixDOFTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 			continue;
 
 		// Steer front wheels
-		Quat steering_rotation = Quat::sRotation(Vec3::sAxisY(), steering_angle);
+		Quat steering_rotation = Quat::sRotation(Vec3::sAxisY(), mSteeringAngle);
 		wheel_constraint->SetTargetOrientationCS(steering_rotation);
 
 		if (brake)
@@ -157,11 +161,11 @@ void VehicleSixDOFTest::PrePhysicsUpdate(const PreUpdateParams &inParams)
 			wheel_constraint->SetTargetAngularVelocityCS(Vec3::sZero());
 			wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Velocity);
 		}
-		else if (speed != 0.0f)
+		else if (mSpeed != 0.0f)
 		{
 			// Front wheel drive, since the motors are applied in the constraint space of the wheel
 			// it is always applied on the X axis
-			wheel_constraint->SetTargetAngularVelocityCS(Vec3(sIsLeftWheel(w)? -speed : speed, 0, 0));
+			wheel_constraint->SetTargetAngularVelocityCS(Vec3(sIsLeftWheel(w)? -mSpeed : mSpeed, 0, 0));
 			wheel_constraint->SetMotorState(EAxis::RotationX, EMotorState::Velocity);
 		}
 		else
@@ -215,3 +219,15 @@ RMat44 VehicleSixDOFTest::GetCameraPivot(float inCameraHeading, float inCameraPi
 	Vec3 right = up.Cross(fwd);
 	return RMat44(Vec4(right, 0), Vec4(up, 0), Vec4(fwd, 0), mCarBody->GetPosition());
 }
+
+void VehicleSixDOFTest::SaveInputState(StateRecorder &inStream) const
+{
+	inStream.Write(mSteeringAngle);
+	inStream.Write(mSpeed);
+}
+
+void VehicleSixDOFTest::RestoreInputState(StateRecorder &inStream)
+{
+	inStream.Read(mSteeringAngle);
+	inStream.Read(mSpeed);
+}

+ 7 - 0
Samples/Tests/Vehicle/VehicleSixDOFTest.h

@@ -15,7 +15,10 @@ public:
 
 	// See: Test
 	virtual void			Initialize() override;
+	virtual void			ProcessInput(const ProcessInputParams &inParams) override;
 	virtual void			PrePhysicsUpdate(const PreUpdateParams &inParams) override;
+	virtual void			SaveInputState(StateRecorder &inStream) const override;
+	virtual void			RestoreInputState(StateRecorder &inStream) override;
 
 	virtual void			GetInitialCamera(CameraState &ioState) const override;
 	virtual RMat44			GetCameraPivot(float inCameraHeading, float inCameraPitch) const override;
@@ -39,4 +42,8 @@ private:
 
 	Body *					mCarBody;
 	Ref<SixDOFConstraint>	mWheels[int(EWheel::Num)];
+
+	// Player input
+	float					mSteeringAngle = 0.0f;
+	float					mSpeed = 0.0f;
 };