View Issue Details
ID | Project | Category | View Status | Date Submitted | Last Update |
---|---|---|---|---|---|
0005992 | The Dark Mod | Coding | public | 02.07.2022 16:19 | 09.07.2022 11:11 |
Reporter | stgatilov | Assigned To | stgatilov | ||
Priority | normal | Severity | normal | Reproducibility | N/A |
Status | resolved | Resolution | fixed | ||
Product Version | TDM 2.10 | ||||
Target Version | TDM 2.11 | Fixed in Version | TDM 2.11 | ||
Summary | 0005992: Experiment with game tics longer than 17 ms | ||||
Description | While uncapped FPS mode works well on high FPS, it boils down to modelling several game tics per frame on low FPS. That's because many things were found wrong with long gametic: 0004924: rope physics 0004983: AI dying randomly (actually, many more) As the result, if PC cannot keep up with game modelling, we are still caught in the "spiral of death". Perhaps we can somehow fix this and use longer gametics? | ||||
Steps To Reproduce | 1) com_fixedTic 1 = uncapped FPS, since otherwise gametics will be fixed 16 ms 2) com_maxTicTimestep 50 = allows to use gametics up to 50 ms long without splitting them into many shorter ones 3) com_maxFps 22 = forces 22 FPS, since otherwise you won't be able to get low FPS consistently | ||||
Tags | No tags attached. | ||||
The internal discussion about the problem is here: https://forums.thedarkmod.com/index.php?/topic/21465-at-lucys-quest-ai-optimisation/&do=findComment&comment=475913 Initially, I tried to actually extend game tics from 17 ms (current upper bound) to 50 ms. In order to make physics stable, I tried to do several "physics subtics" per one game tic. Moreover, I did all subtics for one entity at once, then switched to another entity, rinse and repeat --- that's just easier to implement. I met many problems with physics this way: * Any force lasts for one Evaluate call, but should last for one game tic --- it starts to matter if we do several physics subtics per fame tic. * Some forces (like grabber) should be reevaluated every physics subtic, evaluating them once per game tic makes them unstable at 50ms step. * Some forces (including grabber and dragging) in fact don't apply force, then set velocity or ??add impulse?? Not clear what should be reapplied and what not... * Movers interact with things on them in some non-obvious way, and this is broken too. I managed to solve the first 3 problems I think (with some mess in the code), but gave up on the last one. From this attempt, I only committed refactoring related to forces on physics: r9976. Some physics-related cleaning. r9977. Minor cleaning. r9980. Now all forces are tagged with identifier, and we never apply force with same identifier more than once. Now when you apply force to physics object, you tag if with "identifier". Applying force with same identifier will replace the old application instead of duplicating it (of course if you do it on same game tic, since all forces are cleared once per game tic). For historical reasons, I'm attaching my patch (based on svn rev 9980) here. 5992_physics_subtics.patch (10,590 bytes)
Index: framework/Session.cpp =================================================================== --- framework/Session.cpp (revision 9980) +++ framework/Session.cpp (working copy) @@ -39,6 +39,11 @@ "Never do more than this number of game tics per one frame. " "When frames take too much time, allow game time to run slower than astronomical time.", 1, 1000); +idCVar com_maxPhysicsTimestep("com_maxPhysicsTimestep", "17", CVAR_SYSTEM | CVAR_INTEGER, + "Timestep in physics modelling must not exceed this number of milliseconds. " + "If game tic takes longer, then several physics timesteps are modelled in one tic.\n" + "Ideally, this cvar should be ceil(com_maxTicTimestep / k) for some k.", +1, 1000); idCVar idSessionLocal::com_maxFPS( "com_maxFPS", "166", CVAR_SYSTEM | CVAR_ARCHIVE | CVAR_INTEGER, "define the maximum FPS cap", 2, 1000 ); idCVar idSessionLocal::com_showDemo("com_showDemo", "0", CVAR_SYSTEM | CVAR_BOOL, ""); idCVar idSessionLocal::com_skipGameDraw( "com_skipGameDraw", "0", CVAR_SYSTEM | CVAR_BOOL, "" ); Index: game/Entity.cpp =================================================================== --- game/Entity.cpp (revision 9980) +++ game/Entity.cpp (working copy) @@ -5400,8 +5400,31 @@ if ( part->physics ) { // run physics - moved = part->physics->Evaluate( endTime - startTime, endTime ); + int deltaTime = endTime - startTime; + int numTics = 1; + if ( deltaTime > com_maxPhysicsTimestep.GetInteger() ) + numTics = ( (deltaTime - 1) / com_maxPhysicsTimestep.GetInteger() ) + 1; + // for AI, force one physics step: that's compatible with interleaved thinking + if ( IsType(idAI::Type) && !((idAI*)this)->IsActiveAF() ) + numTics = 1; + moved = false; + for (int s = 0; s < numTics; s++) { + int ticBeg = startTime + deltaTime * (s + 0) / numTics; + int ticEnd = startTime + deltaTime * (s + 1) / numTics; + + // hack: update grabber velocity/force every physics subtic + // also update player's push force (it sets velocity) + if ( gameLocal.m_Grabber->GetSelected() == this ) + gameLocal.m_Grabber->ReEvaluate( ticEnd ); + if ( m_pushedBy.IsValid() && m_pushedBy.GetEntity()->IsType(idPlayer::Type) ) + ((idPlayer*)m_pushedBy.GetEntity())->GetPlayerPhysics()->ReEvaluatePushForce( ticEnd ); + // TODO: do this for all forces acting on the physics object? + + if ( part->physics->Evaluate( ticEnd - ticBeg, ticEnd ) ) + moved = true; + } + // check if the object is blocked blockingEntity = part->physics->GetBlockingEntity(); if ( blockingEntity ) { Index: game/Force_Grab.cpp =================================================================== --- game/Force_Grab.cpp (revision 9980) +++ game/Force_Grab.cpp (working copy) @@ -628,6 +628,20 @@ /* ================ +CForce_Grab::ReEvaluate +================ +*/ +void CForce_Grab::ReEvaluate( int time ) { + // Evaluate only sets velocities and applies forces + // so we can call if as many times as we want on every game tic + // calling it after every physics substep allows to make grabbing stable + // (even on low FPS = several physics subtics per game tic) + Evaluate( time ); +} + + +/* +================ CForce_Grab::RemovePhysics ================ */ Index: game/Force_Grab.h =================================================================== --- game/Force_Grab.h (revision 9980) +++ game/Force_Grab.h (working copy) @@ -74,6 +74,7 @@ public: // common force interface virtual void Evaluate( int time ) override; + virtual void ReEvaluate( int time ) override; virtual void RemovePhysics( const idPhysics *phys ) override; protected: Index: game/gamesys/SysCvar.h =================================================================== --- game/gamesys/SysCvar.h (revision 9980) +++ game/gamesys/SysCvar.h (working copy) @@ -268,6 +268,7 @@ extern idCVar cv_weapon_next_on_empty; // physics +extern idCVar com_maxPhysicsTimestep; extern idCVar cv_collision_damage_scale_vert; extern idCVar cv_collision_damage_scale_horiz; extern idCVar cv_dragged_item_highlight; Index: game/Grabber.cpp =================================================================== --- game/Grabber.cpp (revision 9980) +++ game/Grabber.cpp (working copy) @@ -617,6 +617,11 @@ return; } +void CGrabber::ReEvaluate( int time ) +{ + m_drag.ReEvaluate( time ); +} + void CGrabber::StartDrag( idPlayer *player, idEntity *newEnt, int bodyID, bool preservePosition ) { idVec3 viewPoint, origin, COM, COMWorld, delta2; Index: game/Grabber.h =================================================================== --- game/Grabber.h (revision 9980) +++ game/Grabber.h (working copy) @@ -52,6 +52,8 @@ **/ void Update( idPlayer *player, bool hold = false, bool preservePosition = false ); + void ReEvaluate( int time ); + void Save( idSaveGame *savefile ) const; void Restore( idRestoreGame *savefile ); Index: game/physics/Force.cpp =================================================================== --- game/physics/Force.cpp (revision 9980) +++ game/physics/Force.cpp (working copy) @@ -75,6 +75,14 @@ /* ================ +idForce::ReEvaluate +================ +*/ +void idForce::ReEvaluate( int time ) { +} + +/* +================ idForce::RemovePhysics ================ */ Index: game/physics/Force.h =================================================================== --- game/physics/Force.h (revision 9980) +++ game/physics/Force.h (working copy) @@ -42,6 +42,9 @@ public: // common force interface // evaluate the force up to the given time virtual void Evaluate( int time ); + // stgatilov: evaluate again, probably refining velocity/force + // this is sed for Force_Grab on low FPS, so that it has full control on very physics subtic + virtual void ReEvaluate( int time ); // removes any pointers to the physics object virtual void RemovePhysics( const idPhysics *phys ); Index: game/physics/Force_Push.cpp =================================================================== --- game/physics/Force_Push.cpp (revision 9980) +++ game/physics/Force_Push.cpp (working copy) @@ -29,7 +29,8 @@ id(0), impactVelocity(vec3_zero), startPushTime(-1), - owner(NULL) + owner(NULL), + pushingByVelocity(false) { lastPushEnt = NULL; memset(&contactInfo, 0, sizeof(contactInfo)); @@ -99,6 +100,8 @@ { if (owner == NULL) return; + pushingByVelocity = false; + if (pushEnt == NULL) { // nothing to do, but update the owning actor's push state @@ -218,6 +221,8 @@ // Apply the mass scale and the acceleration scale to the capped velocity physics->SetLinearVelocity(pushVelocity * velocity * accelScale * massScale * entityScale); + // stgatilov: in case we model several physics subtics per game tic, reset this velocity constantly + pushingByVelocity = true; DM_LOG(LC_MOVEMENT, LT_INFO)LOGSTRING("Pushing obstacle %s\r", pushEnt->name.c_str()); @@ -242,6 +247,15 @@ pushEnt = NULL; } +void CForcePush::ReEvaluate( int time ) +{ + if (pushingByVelocity) { + // we need to reset object's velocity every physics subtic + pushEnt = lastPushEnt.GetEntity(); + Evaluate(time); + } +} + void CForcePush::SetOwnerIsPushing(bool isPushing) { // Update the owning actor's push state if it has changed Index: game/physics/Force_Push.h =================================================================== --- game/physics/Force_Push.h (revision 9980) +++ game/physics/Force_Push.h (working copy) @@ -44,6 +44,7 @@ public: // common force interface virtual void Evaluate( int time ) override; + virtual void ReEvaluate( int time ) override; private: void SetOwnerIsPushing(bool isPushing); @@ -56,6 +57,7 @@ trace_t contactInfo; // the contact info of the object we're pushing idVec3 impactVelocity; // the velocity the owner had at impact time int startPushTime; // the time we started to push the physics object + bool pushingByVelocity; // true if we need to constantly set velocity (false if we just apply impulses) idEntity* owner; // the owning entity }; Index: game/physics/Physics_AF.cpp =================================================================== --- game/physics/Physics_AF.cpp (revision 9980) +++ game/physics/Physics_AF.cpp (working copy) @@ -6686,8 +6686,9 @@ // we'll take them into account on the next physics subtic body->current->externalForce.Zero(); body->next->externalForce.Zero(); - // drop list of forces - body->forceApplications.Clear(); + // drop list of forces if this is the last Evaluate on the current game tic + if ( endTimeMSec == gameLocal.time ) + body->forceApplications.Clear(); } // apply contact force to other entities Index: game/physics/Physics_Player.cpp =================================================================== --- game/physics/Physics_Player.cpp (revision 9980) +++ game/physics/Physics_Player.cpp (working copy) @@ -3583,6 +3583,16 @@ /* ================ +idPhysics_Player::ReEvaluatePushForce +================ +*/ +void idPhysics_Player::ReEvaluatePushForce( int time ) { + // stgatilov: update push force, letting it set desired velocity again + m_PushForce->ReEvaluate( time ); +} + +/* +================ idPhysics_Player::UpdateTime ================ */ Index: game/physics/Physics_Player.h =================================================================== --- game/physics/Physics_Player.h (revision 9980) +++ game/physics/Physics_Player.h (working copy) @@ -243,6 +243,8 @@ bool CheckPushEntity(idEntity *entity); // grayman #4603 void ClearPushEntity(); // grayman #4603 + void ReEvaluatePushForce( int time ); + private: // player physics state playerPState_t current; Index: game/physics/Physics_RigidBody.cpp =================================================================== --- game/physics/Physics_RigidBody.cpp (revision 9980) +++ game/physics/Physics_RigidBody.cpp (working copy) @@ -1672,9 +1672,11 @@ current.i.position + centerOfMass * current.i.orientation, &externalForce, &externalTorque, &externalForcePoint ); - // end of game tic: clear force list - // forces will not show up on next game tic unless reapplied via AddForce - current.forceApplications.Clear(); + if ( endTimeMSec == gameLocal.time ) { + // end of game tic: clear force list + // forces will not show up on next game tic unless reapplied via AddForce + current.forceApplications.Clear(); + } if ( hasMaster ) { oldOrigin = current.i.position; |
|
Then I decided to choose cleaner, easier and safer approach with physics, although it might bring less performance benefits. Instead of running physics many times per otherwise normal game tic, I'd run many game tics per frame as before, but skip the toughest work during some game tics. So now if FPS is low and several tics are modelled, we treat all tics except first one as "minor" and can skip some processing during them. Right now I skip only the toughest part: AI thinking. r9981. Allow AIs to think only on the first gametic per frame (controlled by com_useMinorTics). Luckily, we have "Interleaved Thinking Optimization" enabled by default for years, so the support for "AI does not think every gametic" is already here. Maybe in future we'll skip more stuff, but I'd say better don't mess with it without strong need. A related problem from long game tics was AIs accidentally dying. It was simply caused by the fact that "Interleaved Thinking Optimization" counts frames instead of game time milliseconds. I changed that and the problem is gone: 0005992. AI interleaved thinking interval is now fixed in ms (instead of in frames). A side-product is that the game now works faster in high-FPS conditions because AIs think once per 160 ms instead of once per 10 frames. |
|
The last change is actually for debugging only, although it is a very important one. r9975. Fixing debug cvars for forcing dormancy and interleaved thinking of AIs. The game has two ways of optimizing AIs: 1) Dormancy --- AI simply does not think/behave until player gets near or something happens. Originally implemented in Doom 3, it is usually disabled in TDM, because dormant AIs stop patrolling. But it can be enabled back with "neverDormant 0" spawnarg, and some maps (old maps and Lucy's Quest) use it. 2) Interleaved Thinking Optimization --- AIs think rarely, like once per 160 ms. But when they do, they fully cover this long timestep. This is enabled by default, and implemented specifically in TDM. For some reason, tdm_ai_opt_forceopt cvar was actually forcing dormancy on AIs, even though tdm_ai_opt_XXX cvars are usually about ITO. I renamed this cvar to tdm_ai_opt_forcedormant to avoid confusion. Also added "-1" value, in which case all AIs never dormant, even if mapper allowed them to. On the other hand, a way to debug ITO issues is necessary, so I implemented new tdm_ai_opt_forceopt cvar. Now it influences ITO in such a way as if player is infinitely far and in different PVS, so AIs behave with max interleaving. But player can be near them, seeing what they do and e.g. what kills them. In fact, I can easily reproduce AI deaths from long gametics with this cvar, while looking directly at dying AI. |
|
Date Modified | Username | Field | Change |
---|---|---|---|
02.07.2022 16:19 | stgatilov | New Issue | |
02.07.2022 16:19 | stgatilov | Status | new => assigned |
02.07.2022 16:19 | stgatilov | Assigned To | => stgatilov |
02.07.2022 16:21 | stgatilov | Steps to Reproduce Updated | |
02.07.2022 16:22 | stgatilov | Relationship added | related to 0004983 |
02.07.2022 16:22 | stgatilov | Relationship added | related to 0004924 |
02.07.2022 16:23 | stgatilov | Relationship added | related to 0004409 |
09.07.2022 10:47 | stgatilov | Note Added: 0014979 | |
09.07.2022 10:47 | stgatilov | File Added: 5992_physics_subtics.patch | |
09.07.2022 10:57 | stgatilov | Note Added: 0014980 | |
09.07.2022 11:11 | stgatilov | Note Added: 0014981 | |
09.07.2022 11:11 | stgatilov | Status | assigned => resolved |
09.07.2022 11:11 | stgatilov | Resolution | open => fixed |
09.07.2022 11:11 | stgatilov | Fixed in Version | => TDM 2.11 |