/* Copyright (C) 2009 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ // Entity state-machine processing code. #include "precompiled.h" #include "Entity.h" #include "EntityTemplate.h" #include "EventHandlers.h" #include "graphics/Model.h" #include "graphics/ObjectEntry.h" #include "graphics/SkeletonAnim.h" #include "graphics/SkeletonAnimDef.h" // Animation duration #include "graphics/Unit.h" #include "ProductionQueue.h" #include "maths/MathUtil.h" #include "Collision.h" #include "LOSManager.h" #include "graphics/Terrain.h" #include "Stance.h" //#include "sound/SoundGroupMgr.h" #include "ps/Game.h" #include "ps/World.h" #include "lib/rand.h" #include "ps/GameSetup/Config.h" enum EGotoSituation { NORMAL = 0, ALREADY_AT_DESTINATION, REACHED_DESTINATION, COLLISION_WITH_DESTINATION, COLLISION_NEAR_DESTINATION, COLLISION_OVERLAPPING_OBJECTS, COLLISION_OTHER, WOULD_LEAVE_MAP, STANCE_DISALLOWS }; bool CEntity::ShouldRun(float distance) { if( !entf_get(ENTF_SHOULD_RUN) ) return false; // tired if( m_staminaCurr <= 0 ) return false; if( distance >= m_runMaxRange ) return false; // don't start running if less than minimum if( distance <= m_runMinRange && !entf_get(ENTF_IS_RUNNING) ) return false; return true; } float CEntity::ChooseMovementSpeed( float distance ) { bool should_run = ShouldRun(distance); float speed = should_run? m_runSpeed : m_speed; const char* anim_name = should_run? "run" : "walk"; // Modify the speed based on the slope of the terrain in our direction (obtained from our x orientation) float angle = m_orientation_unclamped.x; int sector = rintf( angle / ((float)M_PI/2) * m_base->m_pitchDivs ); speed -= sector * m_base->m_pitchValue; entf_set_to(ENTF_IS_RUNNING, should_run); if ( m_actor ) { if ( !m_actor->IsPlayingAnimation( anim_name ) ) { m_actor->SetAnimationState( anim_name, false, speed, 0.f, false, L"" ); // Animation desync m_actor->GetModel().Update( rand( 0, 1000 ) / 1000.0f ); } } return speed; } // Does all the shared processing for line-of-sight gotos int CEntity::ProcessGotoHelper( CEntityOrder* current, int timestep_millis, HEntity& collide, float& timeLeft ) { float timestep=timestep_millis/1000.0f; CVector2D delta; delta.x = (float)current->m_target_location.x - m_position.X; delta.y = (float)current->m_target_location.y - m_position.Z; float len = delta.Length(); if( len < 0.01f ) return( ALREADY_AT_DESTINATION ); // Curve smoothing. // Here there be trig. float speed = ChooseMovementSpeed( len ); float scale = speed * timestep; // Note: Easy optimization: flag somewhere that this unit // is already pointing the way, and don't do this // trig every time.right m_targetorientation = atan2( delta.x, delta.y ); float deltatheta = m_targetorientation - (float)m_orientation.Y; while( deltatheta > (float)M_PI ) deltatheta -= 2 * (float)M_PI; while( deltatheta < -(float)M_PI ) deltatheta += 2 * (float)M_PI; if( fabs( deltatheta ) > 0.01f ) { if ( m_turningRadius != 0 ) { float maxTurningSpeed = ( speed / m_turningRadius ) * timestep; deltatheta = clamp( deltatheta, -maxTurningSpeed, maxTurningSpeed ); } m_orientation.Y = m_orientation.Y + deltatheta; m_ahead.x = sin( m_orientation.Y ); m_ahead.y = cos( m_orientation.Y ); // We can only really attempt to smooth paths the pathfinder // has flagged for us. If the turning-radius calculations are // applied to other types of waypoint, weirdness happens. // Things like an entity trying to walk to a point inside // his turning radius (which he can't do directly, so he'll // orbit the point indefinitely), or just massive deviations // making the paths we calculate useless. // It's also painful trying to watch two entities resolve their // collision when they're both bound by turning constraints. // So, as a compromise for the look of the thing, we'll just turn in // place until we're looking the right way. At least, that's what // seems logical. But in most cases that looks worse. So actually, // let's not. if( current->m_type != CEntityOrder::ORDER_GOTO_SMOOTHED ) m_orientation.Y = m_targetorientation; } else { m_ahead = delta / len; m_orientation.Y = m_targetorientation; } UpdateXZOrientation(); if( m_bounds && m_bounds->m_type == CBoundingObject::BOUND_OABB ) ((CBoundingBox*) m_bounds)->SetOrientation( m_ahead ); EGotoSituation rc = NORMAL; if( scale > len ) { // Reached destination. Calculate how much time we have left for the next order. scale = len; timeLeft = timestep - (len / speed); rc = REACHED_DESTINATION; } delta = m_ahead * scale; // What would happen if we moved forward a little? m_position.X += delta.x; m_position.Z += delta.y; if( m_bounds ) { m_bounds->SetPosition( m_position.X, m_position.Z ); // For now, ignore passThroughAllies for low-level movement (but leave it on for long-range // pathfinding); ideally we will enable pass-through-allies only for the long-range pathing // and when the unit is moving to assume its place in a formation, since it looks bad to have // units stand on each other otherwise. collide = GetCollisionObject( this, false ); if( collide ) { // We'd hit something. Let's not. m_position.X -= delta.x; m_position.Z -= delta.y; m_bounds->m_pos -= delta; // Is it too late to avoid the collision? if( collide->m_bounds->Intersects( m_bounds ) ) { // Yes. Oh dear. That can't be good. // This really shouldn't happen in the current build. //debug_assert( false && "Overlapping objects" ); // Erm... do nothing? return( COLLISION_OVERLAPPING_OBJECTS ); } // No. Is our destination within the obstacle? if( collide->m_bounds->Contains( current->m_target_location ) ) return( COLLISION_WITH_DESTINATION ); // No. Are we nearing our destination, do we wish to stop there, and is it obstructed? if( ( m_orderQueue.size() == 1 ) && ( len <= 10.0f ) ) { CBoundingCircle destinationObs( current->m_target_location.x, current->m_target_location.y, m_bounds->m_radius, 0.0f ); if( GetCollisionObject( &destinationObs ) ) { // Yes. (Chances are a bunch of units were tasked to the same destination) return( COLLISION_NEAR_DESTINATION ); } } // No? return( COLLISION_OTHER ); } } // Will we step off the map? if( !g_Game->GetWorld()->GetTerrain()->IsOnMap( m_position.X, m_position.Z ) ) { // Yes. That's not a particularly good idea, either. m_position.X -= delta.x; m_position.Z -= delta.y; if( m_bounds ) m_bounds->SetPosition( m_position.X, m_position.Z ); // All things being equal, we should only get here while on a collision path // (No destination should be off the map) return( WOULD_LEAVE_MAP ); } // Does our stance not allow us to go there? if( current->m_source==CEntityOrder::SOURCE_UNIT_AI && !m_stance->CheckMovement( m_position ) ) { m_position.X -= delta.x; m_position.Z -= delta.y; if( m_bounds ) m_bounds->SetPosition( m_position.X, m_position.Z ); return( STANCE_DISALLOWS ); } // No. I suppose it's OK to go there, then. *disappointed* return( rc ); } // Go towards a waypoint, or repath if there is an obstacle in the way bool CEntity::ProcessGotoNoPathing( CEntityOrder* current, int timestep_millis ) { HEntity collide; float timeLeft; switch( ProcessGotoHelper( current, timestep_millis, collide, timeLeft ) ) { case ALREADY_AT_DESTINATION: { // If on a collision path; decide where to go next. Otherwise, proceed to the next waypoint. if( current->m_type == CEntityOrder::ORDER_GOTO_COLLISION ) Repath(); else m_orderQueue.pop_front(); return( false ); } case REACHED_DESTINATION: { // Start along the next segment of the path, if one exists m_orderQueue.pop_front(); if( !m_orderQueue.empty() ) { CEntityOrder* newOrder = &m_orderQueue.front(); switch( newOrder->m_type ) { case CEntityOrder::ORDER_GOTO_NOPATHING: case CEntityOrder::ORDER_GOTO_COLLISION: case CEntityOrder::ORDER_GOTO_SMOOTHED: { int newTimestep = int(timeLeft * 1000.0f); return( ProcessGotoNoPathing( newOrder, newTimestep ) ); } } } return( false ); } case COLLISION_OVERLAPPING_OBJECTS: { return( false ); } case COLLISION_WITH_DESTINATION: { // We're as close as we can get... m_orderQueue.pop_front(); return( false ); } case COLLISION_NEAR_DESTINATION: { // Here's a weird idea: (I hope it works) // Spiral round the destination until a free point is found. CBoundingCircle destinationObs( current->m_target_location.x, current->m_target_location.y, m_bounds->m_radius, 0.0f ); float interval = destinationObs.m_radius; float r = interval, theta = 0.0f, delta; float _x = current->m_target_location.x, _y = current->m_target_location.y; while( true ) { delta = interval / r; theta += delta; r += ( interval * delta ) / ( 2 * (float)M_PI ); destinationObs.SetPosition( _x + r * cosf( theta ), _y + r * sinf( theta ) ); if( !GetCollisionObject( &destinationObs ) ) break; } // Reset our destination current->m_target_location.x = _x + r * cosf( theta ); current->m_target_location.y = _y + r * sinf( theta ); return( false ); } case COLLISION_OTHER: { // Path around it. CEntityOrder avoidance; avoidance.m_type = CEntityOrder::ORDER_GOTO_COLLISION; CVector2D right; right.x = m_ahead.y; right.y = -m_ahead.x; CVector2D avoidancePosition; // Which is the shortest diversion, going left or right? // (Weight a little towards the right, to stop both units dodging the same way) if( ( collide->m_bounds->m_pos - m_bounds->m_pos ).Dot( right ) < 1 ) { // Turn right. avoidancePosition = collide->m_bounds->m_pos + right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } else { // Turn left. avoidancePosition = collide->m_bounds->m_pos - right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } // Create a short path representing this detour avoidance.m_target_location = avoidancePosition; if( current->m_type == CEntityOrder::ORDER_GOTO_COLLISION ) m_orderQueue.pop_front(); m_orderQueue.push_front( avoidance ); return( false ); } case WOULD_LEAVE_MAP: { // Just stop here, Repath if necessary. m_orderQueue.pop_front(); return( false ); } case STANCE_DISALLOWS: { return( false ); // The stance will have cleared our order queue already } default: return( false ); } } // Go towards a waypoint, unless you can perform a contact action bool CEntity::ProcessGotoNoPathingContact( CEntityOrder* current, int timestep_millis ) { CEntityOrder* contact_order = 0; CEntityOrders::iterator it = m_orderQueue.begin(); for(; it != m_orderQueue.end(); ++it) { if( it->m_type == CEntityOrder::ORDER_CONTACT_ACTION || it->m_type == CEntityOrder::ORDER_CONTACT_ACTION_NOPATHING ) { contact_order = &(*it); break; } } if( ProcessContactAction(contact_order, timestep_millis, false ) ) { // We are in range to do our action; pop off all the pathing orders while( m_orderQueue.front().m_type != CEntityOrder::ORDER_CONTACT_ACTION && m_orderQueue.front().m_type != CEntityOrder::ORDER_CONTACT_ACTION_NOPATHING ) { m_orderQueue.pop_front(); } return true; } else { return ProcessGotoNoPathing( current, timestep_millis ); } } // Handles processing common to (at the moment) gather and melee attack actions bool CEntity::ProcessContactAction( CEntityOrder* current, int UNUSED(timestep_millis), bool repath_if_needed ) { if( m_actions.find( current->m_action ) == m_actions.end() ) { return false; // we've been tasked as part of a group but we can't do this action } SEntityAction* action = &m_actions[current->m_action]; CEntityOrder::EOrderType transition = CEntityOrder::ORDER_CONTACT_ACTION_NOPATHING; HEntity target = current->m_target_entity; if( !target || !target->m_extant ) { PopOrder(); if( m_orderQueue.empty() && target.IsValid() ) { CEventTargetExhausted evt( target, action->m_Id ); DispatchEvent( &evt ); } return false; } if( current->m_source != CEntityOrder::SOURCE_TRIGGERS && g_Game->GetWorld()->GetLOSManager()->GetUnitStatus( target, m_player ) == UNIT_HIDDEN ) { PopOrder(); return false; } current->m_target_location = target->m_position; float Distance = Distance2D(current->m_target_location); if( Distance < action->m_MaxRange ) { current->m_type = transition; entf_clear(ENTF_IS_RUNNING); return true; } else if (repath_if_needed) { if( current->m_source == CEntityOrder::SOURCE_UNIT_AI && !m_stance->AllowsMovement() ) { PopOrder(); return false; // We're not allowed to move at all by the current stance } ChooseMovementSpeed( Distance ); // The pathfinder will push its result back into this unit's queue and // add back the current order at the end with the transition type. current->m_type = transition; // g_Pathfinder.RequestContactPath( me, current, action->m_MaxRange ); return true; } else { return false; } } bool CEntity::ProcessContactActionNoPathing( CEntityOrder* current, int timestep_millis ) { if( m_actions.find( current->m_action ) == m_actions.end() ) { return false; // we've been tasked as part of a group but we can't do this action } if( !m_actor ) { return( false ); // shouldn't happen, but having no actor would mean we have no animation } CEventContactAction contactEvent( current->m_target_entity, current->m_action ); SEntityAction* action = &m_actions[current->m_action]; CStr& animation = action->m_Animation; HEntity target = current->m_target_entity; if( m_fsm_cyclepos != NOT_IN_CYCLE ) { int nextpos = m_fsm_cyclepos + timestep_millis * 2; if( ( m_fsm_cyclepos <= action->m_Speed ) && ( nextpos > action->m_Speed ) ) { // const size_t soundGroupIndex = m_base->m_SoundGroupTable[animation]; // g_soundGroupMgr->PlayNext(soundGroupIndex, m_position); if(!DispatchEvent( &contactEvent )) { // Cancel current order entf_clear(ENTF_IS_RUNNING); entf_clear(ENTF_SHOULD_RUN); m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" ); PopOrder(); if( m_orderQueue.empty() && target.IsValid() ) { CEventTargetExhausted evt( target, action->m_Id ); DispatchEvent( &evt ); } return( false ); } } if( nextpos >= ( action->m_Speed * 2 ) ) { // End of cycle. m_fsm_cyclepos = NOT_IN_CYCLE; return( false ); } // Otherwise, increment position. m_fsm_cyclepos = nextpos; return( false ); } // Target's dead (or exhausted), or we cancelled? Then our work here is done. if( !target || !target->m_extant || g_Game->GetWorld()->GetLOSManager()->GetUnitStatus( target, m_player ) == UNIT_HIDDEN ) { PopOrder(); if( m_orderQueue.empty() && target.IsValid() ) { CEventTargetExhausted evt( target, action->m_Id ); DispatchEvent( &evt ); } entf_clear(ENTF_IS_RUNNING); entf_clear(ENTF_SHOULD_RUN); return( false ); } CVector2D delta = CVector2D(target->m_position) - CVector2D(m_position); float deltaLength = delta.Length(); float adjRange = action->m_MaxRange + m_bounds->m_radius + target->m_bounds->m_radius; if( action->m_MinRange > 0.0f ) { float adjMinRange = action->m_MinRange + m_bounds->m_radius + target->m_bounds->m_radius; if( delta.within( adjMinRange ) ) { // Too close... avoid it if allowed by the current stance. if( current->m_source == CEntityOrder::SOURCE_UNIT_AI && !m_stance->AllowsMovement() ) { PopOrder(); m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" ); return false; // We're not allowed to move at all by the current stance } entf_set(ENTF_SHOULD_RUN); ChooseMovementSpeed( action->m_MinRange ); // The pathfinder will push its result in front of the current order // if( !g_Pathfinder.RequestAvoidPath( me, current, action->m_MinRange + 2.0f ) ) // { // m_actor->SetAnimationState( "idle", false, 1.f, 0.f, false, L"" ); // Nothing we can do.. maybe we'll find a better target // PopOrder(); // } return false; } } if( !delta.within( adjRange ) ) { // Too far away at the moment, chase after the target if allowed... if( current->m_source == CEntityOrder::SOURCE_UNIT_AI && !m_stance->AllowsMovement() ) { PopOrder(); return false; } // We're aiming to end up at a location just inside our maximum range // (is this good enough?) delta = delta.Normalize() * ( adjRange - m_bounds->m_radius ); ChooseMovementSpeed(deltaLength); current->m_target_location = (CVector2D)target->m_position - delta; HEntity collide; float timeLeft; switch( ProcessGotoHelper( current, timestep_millis, collide, timeLeft ) ) { case REACHED_DESTINATION: case ALREADY_AT_DESTINATION: case COLLISION_WITH_DESTINATION: case WOULD_LEAVE_MAP: case STANCE_DISALLOWS: // Not too far any more... break; case NORMAL: // May or may not be close enough, check... // (Assuming the delta above will never take us within minimum range) delta = target->m_position - m_position; if( delta.within( adjRange ) ) break; // Otherwise, continue chasing return( false ); default: // We have a collision. Path around it. CEntityOrder avoidance; avoidance.m_type = CEntityOrder::ORDER_GOTO_COLLISION; CVector2D right; right.x = m_ahead.y; right.y = -m_ahead.x; CVector2D avoidancePosition; // Which is the shortest diversion, going left or right? // (Weight a little towards the right, to stop both units dodging the same way) if( ( collide->m_bounds->m_pos - m_bounds->m_pos ).Dot( right ) < 1 ) { // Turn right. avoidancePosition = collide->m_bounds->m_pos + right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } else { // Turn left. avoidancePosition = collide->m_bounds->m_pos - right * ( collide->m_bounds->m_radius + m_bounds->m_radius * 2.5f ); } // Create a short path representing this detour avoidance.m_target_location = avoidancePosition; if( current->m_type == CEntityOrder::ORDER_GOTO_COLLISION ) m_orderQueue.pop_front(); m_orderQueue.push_front( avoidance ); return( false ); } } else { // Close enough, but turn to face them. m_orientation.Y = atan2( delta.x, delta.y ); m_ahead = delta.Normalize(); entf_clear(ENTF_IS_RUNNING); } m_actor->SetAnimationState( animation, false, 1000.f / (float)action->m_Speed, 0.f, false, L"" ); // m_actor->SetAnimationSync( (float)( action->m_Speed / 2) / 1000.f ); m_fsm_cyclepos = 0; return( false ); } bool CEntity::ProcessGoto( CEntityOrder* current, int UNUSED(timestep_millis) ) { CVector2D pos( m_position.X, m_position.Z ); CVector2D path_to = current->m_target_location; float Distance = ( path_to - pos ).Length(); CEntityOrder::EOrderSource source = current->m_source; m_orderQueue.pop_front(); // pop_front may delete 'current', so we mustn't use it after this point // Let's just check we're going somewhere... if( Distance < 0.1f ) { //entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); return( false ); } ChooseMovementSpeed( Distance ); // The pathfinder will push its result back into this unit's queue. // g_Pathfinder.RequestPath( me, path_to, source ); return( true ); } bool CEntity::ProcessGotoWaypoint( CEntityOrder* current, int UNUSED(timestep_milli), bool contact ) { CVector2D pos( m_position.X, m_position.Z ); CVector2D path_to = current->m_target_location; float Distance = ( path_to - pos ).Length(); CEntityOrder::EOrderSource source = current->m_source; float pathfinder_radius = current->m_pathfinder_radius; m_orderQueue.pop_front(); // pop_front may delete 'current', so we mustn't use it after this point // Let's just check we're going somewhere... if( Distance < 0.1f ) { entf_clear(ENTF_IS_RUNNING); //entf_clear(ENTF_SHOULD_RUN); return( false ); } ChooseMovementSpeed( Distance ); #ifdef USE_DCDT //Kai: invoking triangulation or original A* pathfinding if(g_TriPathfind) { g_Pathfinder.RequestTriangulationPath( me, path_to, contact, pathfinder_radius, source ); } else #endif // USE_DCDT { // g_Pathfinder.RequestLowLevelPath( me, path_to, contact, pathfinder_radius, source ); } return( true ); } bool CEntity::ProcessPatrol( CEntityOrder* current, int UNUSED(timestep_millis) ) { CEntityOrder this_segment; CEntityOrder repeat_patrol; // Duplicate the patrol order, push one copy onto the start of our order queue // (that's the path we'll be taking next) and one copy onto the end of the // queue (to keep us patrolling) this_segment.m_type = CEntityOrder::ORDER_GOTO; this_segment.m_pathfinder_radius = current->m_pathfinder_radius; repeat_patrol.m_type = CEntityOrder::ORDER_PATROL; repeat_patrol.m_pathfinder_radius = current->m_pathfinder_radius; m_orderQueue.pop_front(); m_orderQueue.push_front( this_segment ); m_orderQueue.push_back( repeat_patrol ); return( true ); } bool CEntity::ProcessProduce( CEntityOrder* order ) { CEventStartProduction evt( order->m_produce_type, order->m_name ); if( DispatchEvent( &evt ) && evt.GetTime() >= 0 ) { m_productionQueue->AddItem( order->m_produce_type, order->m_name, evt.GetTime() ); } return( false ); }