forked from 0ad/0ad
d6c4bf3302
This was SVN commit r2350.
556 lines
17 KiB
C++
Executable File
556 lines
17 KiB
C++
Executable File
// 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 "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.
|
|
|
|
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 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;
|
|
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.
|
|
|
|
g_Pathfinder.requestContactPath( me, current->m_data[0].entity, transition );
|
|
|
|
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_actor->GetRandomAnimation( "idle" ) );
|
|
}
|
|
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 )
|
|
{
|
|
// 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 + ( action->m_Speed * m_fsm_animation->m_ActionPos2 * 2 );
|
|
if( m_fsm_anipos2 < 0 )
|
|
{
|
|
// 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::processGoto( CEntityOrder* current, size_t timestep_millis )
|
|
{
|
|
float timestep=timestep_millis/1000.0f;
|
|
|
|
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" );
|
|
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 timestep_millis )
|
|
{
|
|
float timestep=timestep_millis/1000.0f;
|
|
|
|
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 );
|
|
}
|