// Entity state-machine processing code. #include "precompiled.h" #include "Entity.h" #include "BaseEntity.h" #include "Model.h" #include "ObjectEntry.h" #include "SkeletonAnimDef.h" // Animation duration #include "Unit.h" #include "MathUtil.h" #include "Collision.h" #include "PathfindEngine.h" #include "Terrain.h" #include "Game.h" enum EGotoSituation { NORMAL = 0, ALREADY_AT_DESTINATION, REACHED_DESTINATION, COLLISION_WITH_DESTINATION, COLLISION_NEAR_DESTINATION, COLLISION_OVERLAPPING_OBJECTS, COLLISION_OTHER, WOULD_LEAVE_MAP }; // Does all the shared processing for line-of-sight gotos uint CEntity::processGotoHelper( CEntityOrder* current, size_t timestep_millis, HEntity& collide ) { float timestep=timestep_millis/1000.0f; CVector2D delta; delta.x = (float)current->m_data[0].location.x - m_position.X; delta.y = (float)current->m_data[0].location.y - m_position.Z; float len = delta.length(); if( len < 0.1f ) return( ALREADY_AT_DESTINATION ); // Curve smoothing. // Here there be trig. float scale = m_speed * timestep; // Note: Easy optimization: flag somewhere that this unit // is already pointing the right way, and don't do this // trig every time. m_targetorientation = atan2( delta.x, delta.y ); float deltatheta = m_targetorientation - (float)m_orientation; while( deltatheta > PI ) deltatheta -= 2 * PI; while( deltatheta < -PI ) deltatheta += 2 * PI; if( fabs( deltatheta ) > 0.01f ) { float maxTurningSpeed = ( m_speed / m_turningRadius ) * timestep; if( deltatheta > 0 ) { m_orientation = m_orientation + MIN( deltatheta, maxTurningSpeed ); } else m_orientation = m_orientation + MAX( deltatheta, -maxTurningSpeed ); m_ahead.x = sin( m_orientation ); m_ahead.y = cos( m_orientation ); // 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, wierdness 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 indefinately), 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 = m_targetorientation; } else { m_ahead = delta / len; m_orientation = m_targetorientation; } if( m_bounds && m_bounds->m_type == CBoundingObject::BOUND_OABB ) ((CBoundingBox*)m_bounds)->setOrientation( m_ahead ); EGotoSituation rc = NORMAL; if( scale > len ) { scale = len; 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 ); collide = getCollisionObject( this ); 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_data[0].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_data[0].location.x, current->m_data[0].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 ); } // No. I suppose it's OK to go there, then. *disappointed* return( rc ); } bool CEntity::processGotoNoPathing( CEntityOrder* current, size_t timestep_millis ) { HEntity collide; switch( processGotoHelper( current, timestep_millis, collide ) ) { 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 COLLISION_OVERLAPPING_OBJECTS: return( false ); case COLLISION_WITH_DESTINATION: // We're here... m_orderQueue.pop_front(); return( false ); case COLLISION_NEAR_DESTINATION: { // Here's a wierd idea: (I hope it works) // Spiral round the destination until a free point is found. CBoundingCircle destinationObs( current->m_data[0].location.x, current->m_data[0].location.y, m_bounds->m_radius, 0.0f ); float interval = destinationObs.m_radius; float r = interval, theta = 0.0f, delta; float _x = current->m_data[0].location.x, _y = current->m_data[0].location.y; while( true ) { delta = interval / r; theta += delta; r += ( interval * delta ) / ( 2 * PI ); destinationObs.setPosition( _x + r * cosf( theta ), _y + r * sinf( theta ) ); if( !getCollisionObject( &destinationObs ) ) break; } // Reset our destination current->m_data[0].location.x = _x + r * cosf( theta ); current->m_data[0].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_data[0].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 ); default: return( false ); } } // Handles processing common to (at the moment) gather and melee attack actions bool CEntity::processContactAction( CEntityOrder* current, size_t UNUSED(timestep_millis), int transition, SEntityAction* action ) { m_orderQueue.pop_front(); if( !current->m_data[0].entity || !current->m_data[0].entity->m_extant ) return( false ); current->m_data[0].location = current->m_data[0].entity->m_position; if( ( current->m_data[0].location - m_position ).length() < action->m_MaxRange ) { (int&)current->m_type = transition; m_orderQueue.push_front(*current); // Seems to be needed since we do a pop above return( true ); } if( m_transition && m_actor ) { CSkeletonAnim* walk = m_actor->GetRandomAnimation( "walk" ); m_actor->GetModel()->SetAnimation( walk, false, m_speed * walk->m_AnimDef->GetDuration() ); // Animation desync m_actor->GetModel()->Update( ( rand() * 1000.0f ) / 1000.0f ); } // 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. (int&)current->m_type = transition; g_Pathfinder.requestContactPath( me, current ); return( true ); } bool CEntity::processContactActionNoPathing( CEntityOrder* current, size_t timestep_millis, const CStr& animation, CScriptEvent* contactEvent, SEntityAction* action ) { if( m_fsm_cyclepos != NOT_IN_CYCLE ) { size_t nextpos = m_fsm_cyclepos + timestep_millis * 2; if( ( m_fsm_cyclepos <= m_fsm_anipos ) && ( nextpos > m_fsm_anipos ) ) { // Start playing. // Start the animation. Actual damage/gather will be done in a // few hundred ms, at the 'action point' of the animation we're // now setting. m_actor->GetModel()->SetAnimation( m_fsm_animation, true, 1000.0f * m_fsm_animation->m_AnimDef->GetDuration() / (float)action->m_Speed, m_fsm_animation ); } if( ( m_fsm_cyclepos <= m_fsm_anipos2 ) && ( nextpos > m_fsm_anipos2 ) ) { // Load the ammunition. m_actor->ShowAmmunition(); } if( ( m_fsm_cyclepos <= action->m_Speed ) && ( nextpos > action->m_Speed ) ) { // Fire! m_actor->HideAmmunition(); DispatchEvent( contactEvent ); // Note that, at the moment, we don't care if the action succeeds or fails - // we could check for failure, then abort the animation. // It depends what we think is worse: stopping an animation halfway through, // or playing the animation without getting a game effect. // Could also check again here if the entity still exists, is in range, etc.. // and cancel if not, but we'll see how it looks without that, first. } 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)? Then our work here is done. if( !current->m_data[0].entity || !current->m_data[0].entity->m_extant ) { m_orderQueue.pop_front(); return( false ); } CVector2D delta = current->m_data[0].entity->m_position - m_position; float adjRange = action->m_MaxRange + m_bounds->m_radius + current->m_data[0].entity->m_bounds->m_radius; if( action->m_MinRange > 0.0f ) { float adjMinRange = action->m_MinRange + m_bounds->m_radius + current->m_data[0].entity->m_bounds->m_radius; if( delta.within( adjMinRange ) ) { // Too close... do nothing. return( false ); } } if( !delta.within( adjRange ) ) { // Too far away at the moment, chase after the target... // We're aiming to end up at a location just inside our maximum range // (is this good enough?) // Play walk for a bit. if( m_actor && ! m_actor->IsPlayingAnimation( "walk" ) ) { CSkeletonAnim* walk = m_actor->GetRandomAnimation( "walk" ); m_actor->GetModel()->SetAnimation( walk, false, m_speed * walk->m_AnimDef->GetDuration() ); // Animation desync m_actor->GetModel()->Update( ( rand() * 1000.0f ) / 1000.0f ); } delta = delta.normalize() * ( adjRange - m_bounds->m_radius ); current->m_data[0].location = (CVector2D)current->m_data[0].entity->m_position - delta; HEntity collide; switch( processGotoHelper( current, timestep_millis, collide ) ) { case ALREADY_AT_DESTINATION: case REACHED_DESTINATION: case COLLISION_WITH_DESTINATION: // 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 = current->m_data[0].entity->m_position - m_position; if( delta.within( adjRange ) ) break; // Otherwise, continue chasing return( false ); default: // 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_data[0].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 = atan2( delta.x, delta.y ); m_ahead = delta.normalize(); } // Pick our animation, calculate the time to play it, and start the timer. m_fsm_animation = m_actor->GetRandomAnimation( animation ); // Here's the idea - we want to be at that animation's event point // when the timer reaches action->m_Speed. The timer increments by 2 every millisecond. // animation->m_actionpos is the time offset into that animation that event // should happen. So... m_fsm_anipos = (size_t)( action->m_Speed * ( 1.0f - 2 * m_fsm_animation->m_ActionPos ) ); // But... if( m_fsm_anipos < 0 ) // (FIXME: m_fsm_anipos is unsigned, so this will never be true...) { // We ought to have started it in the past. Oh well. // Here's what we'll do: play it now, and advance it to // the point it should be by now. m_actor->GetModel()->SetAnimation( m_fsm_animation, true, 1000.0f * m_fsm_animation->m_AnimDef->GetDuration() / (float)action->m_Speed, m_actor->GetRandomAnimation( "idle" ) ); m_actor->GetModel()->Update( action->m_Speed * ( m_fsm_animation->m_ActionPos / 1000.0f - 0.0005f ) ); } else { // If we've just transitioned, play idle. Otherwise, let the previous animation complete, if it // hasn't already. if( m_transition ) m_actor->SetRandomAnimation( "idle" ); } // Load time needs to be animation->m_ActionPos2 ms after the start of the animation. m_fsm_anipos2 = m_fsm_anipos + (size_t)( action->m_Speed * m_fsm_animation->m_ActionPos2 * 2 ); if( m_fsm_anipos2 < 0 ) // (FIXME: m_fsm_anipos2 is unsigned, so this will never be true...) { // Load now. m_actor->ShowAmmunition(); } m_fsm_cyclepos = 0; return( false ); } bool CEntity::processAttackMelee( CEntityOrder* current, size_t timestep_millis ) { return( processContactAction( current, timestep_millis, CEntityOrder::ORDER_ATTACK_MELEE_NOPATHING, &m_melee ) ); } bool CEntity::processAttackMeleeNoPathing( CEntityOrder* current, size_t timestep_milli ) { CEventAttack evt( current->m_data[0].entity ); if( !m_actor ) return( false ); return( processContactActionNoPathing( current, timestep_milli, "melee", &evt, &m_melee ) ); } bool CEntity::processGather( CEntityOrder* current, size_t timestep_millis ) { return( processContactAction( current, timestep_millis, CEntityOrder::ORDER_GATHER_NOPATHING, &m_gather ) ); } bool CEntity::processGatherNoPathing( CEntityOrder* current, size_t timestep_millis ) { CEventGather evt( current->m_data[0].entity ); if( !m_actor ) return( false ); return( processContactActionNoPathing( current, timestep_millis, "gather", &evt, &m_gather ) ); } bool CEntity::processHeal( CEntityOrder* current, size_t timestep_millis ) { return( processContactAction( current, timestep_millis, CEntityOrder::ORDER_HEAL_NOPATHING, &m_heal ) ); } bool CEntity::processHealNoPathing( CEntityOrder* current, size_t timestep_millis ) { CEventHeal evt( current->m_data[0].entity ); if( !m_actor ) return( false ); return( processContactActionNoPathing( current, timestep_millis, "heal", &evt, &m_heal ) ); } bool CEntity::processGeneric( CEntityOrder* current, size_t timestep_millis ) { int id = current->m_data[1].data; if( m_actions.find( id ) == 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[id]; return( processContactAction( current, timestep_millis, CEntityOrder::ORDER_GENERIC_NOPATHING, &action ) ); } bool CEntity::processGenericNoPathing( CEntityOrder* current, size_t timestep_millis ) { int id = current->m_data[1].data; if( m_actions.find( id ) == 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[id]; CEventGeneric evt( current->m_data[0].entity, id ); if( !m_actor ) return( false ); return( processContactActionNoPathing( current, timestep_millis, action.m_Animation, &evt, &action ) ); } bool CEntity::processGoto( CEntityOrder* current, size_t UNUSED(timestep_millis) ) { // float timestep=timestep_millis/1000.0f; // janwas: currently unused CVector2D pos( m_position.X, m_position.Z ); CVector2D path_to = current->m_data[0].location; m_orderQueue.pop_front(); // Let's just check we're going somewhere... if( ( path_to - pos ).length() < 0.1f ) return( false ); if( m_transition && m_actor ) { CSkeletonAnim* walk = m_actor->GetRandomAnimation( "walk" ); if( walk ) m_actor->GetModel()->SetAnimation( walk, false, m_speed * walk->m_AnimDef->GetDuration() ); // Animation desync m_actor->GetModel()->Update( ( rand() * 1000.0f ) / 1000.0f ); } // The pathfinder will push its result back into this unit's queue. g_Pathfinder.requestPath( me, path_to ); return( true ); } bool CEntity::processPatrol( CEntityOrder* current, size_t UNUSED(timestep_millis) ) { // float timestep=timestep_millis/1000.0f; // janwas: currently unused 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_data[0] = current->m_data[0]; repeat_patrol.m_type = CEntityOrder::ORDER_PATROL; repeat_patrol.m_data[0] = current->m_data[0]; m_orderQueue.pop_front(); m_orderQueue.push_front( this_segment ); m_orderQueue.push_back( repeat_patrol ); return( true ); }