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

Minor code clean up and bug fixes for DetourCrowd implementation.
Avoid using multiple vectors to hold the same set of objects in the DetourCrowd sample.

Yao Wei Tjong 姚伟忠 10 жил өмнө
parent
commit
6b72c4412a

+ 84 - 98
Source/Samples/39_CrowdNavigation/CrowdNavigation.cpp

@@ -57,6 +57,8 @@
 
 #include <Urho3D/DebugNew.h>
 
+const String INSTRUCTION("instructionText");
+
 DEFINE_APPLICATION_MAIN(CrowdNavigation)
 
 CrowdNavigation::CrowdNavigation(Context* context) :
@@ -121,10 +123,10 @@ void CrowdNavigation::CreateScene()
     light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f));
 
     // Create randomly sized boxes. If boxes are big enough, make them occluders
-    Vector< SharedPtr<Node> > boxes;
+    Node* boxGroup = scene_->CreateChild("Boxes");
     for (unsigned i = 0; i < 20; ++i)
     {
-        Node* boxNode = scene_->CreateChild("Box");
+        Node* boxNode = boxGroup->CreateChild("Box");
         float size = 1.0f + Random(10.0f);
         boxNode->SetPosition(Vector3(Random(80.0f) - 40.0f, size * 0.5f, Random(80.0f) - 40.0f));
         boxNode->SetScale(size);
@@ -133,46 +135,43 @@ void CrowdNavigation::CreateScene()
         boxObject->SetMaterial(cache->GetResource<Material>("Materials/Stone.xml"));
         boxObject->SetCastShadows(true);
         if (size >= 3.0f)
-        {
             boxObject->SetOccluder(true);
-            boxes.Push(SharedPtr<Node>(boxNode));
-        }
     }
 
     // Create a DynamicNavigationMesh component to the scene root
-    navMesh_ = scene_->CreateComponent<DynamicNavigationMesh>();
+    DynamicNavigationMesh* navMesh = scene_->CreateComponent<DynamicNavigationMesh>();
     // Enable drawing debug geometry for obstacles and off-mesh connections
-    navMesh_->SetDrawObstacles(true);
-    navMesh_->SetDrawOffMeshConnections(true);
+    navMesh->SetDrawObstacles(true);
+    navMesh->SetDrawOffMeshConnections(true);
     // Set the agent height large enough to exclude the layers under boxes
-    navMesh_->SetAgentHeight(10.0f);
+    navMesh->SetAgentHeight(10.0f);
     // Set nav mesh cell height to minimum (allows agents to be grounded)
-    navMesh_->SetCellHeight(0.05f);
+    navMesh->SetCellHeight(0.05f);
     // Create a Navigable component to the scene root. This tags all of the geometry in the scene as being part of the
     // navigation mesh. By default this is recursive, but the recursion could be turned off from Navigable
     scene_->CreateComponent<Navigable>();
     // Add padding to the navigation mesh in Y-direction so that we can add objects on top of the tallest boxes
     // in the scene and still update the mesh correctly
-    navMesh_->SetPadding(Vector3(0.0f, 10.0f, 0.0f));
+    navMesh->SetPadding(Vector3(0.0f, 10.0f, 0.0f));
     // Now build the navigation geometry. This will take some time. Note that the navigation mesh will prefer to use
     // physics geometry from the scene nodes, as it often is simpler, but if it can not find any (like in this example)
     // it will use renderable geometry instead
-    navMesh_->Build();
+    navMesh->Build();
 
     // Create an off-mesh connection to each box to make them climbable (tiny boxes are skipped). A connection is built from 2 nodes.
     // Note that OffMeshConnections must be added before building the navMesh, but as we are adding Obstacles next, tiles will be automatically rebuilt.
     // Creating connections post-build here allows us to use FindNearestPoint() to procedurally set accurate positions for the connection
-    CreateBoxOffMeshConnections(boxes);
+    CreateBoxOffMeshConnections(navMesh, boxGroup);
 
     // Create some mushrooms as obstacles. Note that obstacles are non-walkable areas
     for (unsigned i = 0; i < 100; ++i)
         CreateMushroom(Vector3(Random(90.0f) - 45.0f, 0.0f, Random(90.0f) - 45.0f));
 
     // Create a DetourCrowdManager component to the scene root
-    crowdManager_ = scene_->CreateComponent<DetourCrowdManager>();
+    scene_->CreateComponent<DetourCrowdManager>();
 
     // Create some movable barrels. We create them as crowd agents, as for moving entities it is less expensive and more convenient than using obstacles
-    CreateMovingBarrels();
+    CreateMovingBarrels(navMesh);
 
     // Create Jack node as crowd agent
     SpawnJack(Vector3(-5.0f, 0.0f, 20.0f));
@@ -183,8 +182,10 @@ void CrowdNavigation::CreateScene()
     Camera* camera = cameraNode_->CreateComponent<Camera>();
     camera->SetFarClip(300.0f);
 
-    // Set an initial position for the camera scene node above the plane
-    cameraNode_->SetPosition(Vector3(0.0f, 5.0f, 0.0f));
+    // Set an initial position for the camera scene node above the plane and looking down
+    cameraNode_->SetPosition(Vector3(0.0f, 50.0f, 0.0f));
+    pitch_ = 80.0f;
+    cameraNode_->SetRotation(Quaternion(pitch_, yaw_, 0.0f));
 }
 
 void CrowdNavigation::CreateUI()
@@ -204,16 +205,17 @@ void CrowdNavigation::CreateUI()
     cursor->SetPosition(graphics->GetWidth() / 2, graphics->GetHeight() / 2);
 
     // Construct new Text object, set string to display and font to use
-    Text* instructionText = ui->GetRoot()->CreateChild<Text>();
+    Text* instructionText = ui->GetRoot()->CreateChild<Text>(INSTRUCTION);
     instructionText->SetText(
         "Use WASD keys to move, RMB to rotate view\n"
         "LMB to set destination, SHIFT+LMB to spawn a Jack\n"
         "CTRL+LMB to teleport main agent\n"
         "MMB to add obstacles or remove obstacles/agents\n"
         "F5 to save scene, F7 to load\n"
-        "Space to toggle debug geometry"
+        "Space to toggle debug geometry\n"
+        "F12 to toggle this instruction text"
     );
-    instructionText->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 12);
+    instructionText->SetFont(cache->GetResource<Font>("Fonts/Anonymous Pro.ttf"), 15);
     // The text has multiple rows. Center them in relation to each other
     instructionText->SetTextAlignment(HA_CENTER);
 
@@ -237,13 +239,15 @@ void CrowdNavigation::SubscribeToEvents()
     // Subscribe HandleUpdate() function for processing update events
     SubscribeToEvent(E_UPDATE, HANDLER(CrowdNavigation, HandleUpdate));
 
-    // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request
-    // debug geometry
+    // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request debug geometry
     SubscribeToEvent(E_POSTRENDERUPDATE, HANDLER(CrowdNavigation, HandlePostRenderUpdate));
 
     // Subscribe HandleCrowdAgentFailure() function for resolving invalidation issues with agents, during which we
     // use a larger extents for finding a point on the navmesh to fix the agent's position
     SubscribeToEvent(E_CROWD_AGENT_FAILURE, HANDLER(CrowdNavigation, HandleCrowdAgentFailure));
+
+    // Subscribe HandleCrowdAgentReposition() function for controlling the animation
+    SubscribeToEvent(E_CROWD_AGENT_REPOSITION, HANDLER(CrowdNavigation, HandleCrowdAgentReposition));
 }
 
 void CrowdNavigation::SpawnJack(const Vector3& pos)
@@ -260,9 +264,8 @@ void CrowdNavigation::SpawnJack(const Vector3& pos)
     // Create a CrowdAgent component and set its height and realistic max speed/acceleration. Use default radius
     CrowdAgent* agent = jackNode->CreateComponent<CrowdAgent>();
     agent->SetHeight(2.0f);
-    agent->SetMaxSpeed(4.0f);
+    agent->SetMaxSpeed(3.0f);
     agent->SetMaxAccel(100.0f);
-    agents_ = crowdManager_->GetActiveAgents(); // Update agents container
 }
 
 void CrowdNavigation::CreateMushroom(const Vector3& pos)
@@ -284,8 +287,9 @@ void CrowdNavigation::CreateMushroom(const Vector3& pos)
     obstacle->SetHeight(mushroomNode->GetScale().y_);
 }
 
-void CrowdNavigation::CreateBoxOffMeshConnections(Vector< SharedPtr<Node> > boxes)
+void CrowdNavigation::CreateBoxOffMeshConnections(DynamicNavigationMesh* navMesh, Node* boxGroup)
 {
+    const Vector<SharedPtr<Node> >& boxes = boxGroup->GetChildren();
     for (unsigned i=0; i < boxes.Size(); ++i)
     {
         Node* box = boxes[i];
@@ -294,9 +298,9 @@ void CrowdNavigation::CreateBoxOffMeshConnections(Vector< SharedPtr<Node> > boxe
 
         // Create 2 empty nodes for the start & end points of the connection. Note that order matters only when using one-way/unidirectional connection.
         Node* connectionStart = box->CreateChild("ConnectionStart");
-        connectionStart->SetWorldPosition(navMesh_->FindNearestPoint(boxPos + Vector3(boxHalfSize, -boxHalfSize, 0))); // Base of box
+        connectionStart->SetWorldPosition(navMesh->FindNearestPoint(boxPos + Vector3(boxHalfSize, -boxHalfSize, 0))); // Base of box
         Node* connectionEnd = connectionStart->CreateChild("ConnectionEnd");
-        connectionEnd->SetWorldPosition(navMesh_->FindNearestPoint(boxPos + Vector3(boxHalfSize, boxHalfSize, 0))); // Top of box
+        connectionEnd->SetWorldPosition(navMesh->FindNearestPoint(boxPos + Vector3(boxHalfSize, boxHalfSize, 0))); // Top of box
 
         // Create the OffMeshConnection component to one node and link the other node
         OffMeshConnection* connection = connectionStart->CreateComponent<OffMeshConnection>();
@@ -304,7 +308,7 @@ void CrowdNavigation::CreateBoxOffMeshConnections(Vector< SharedPtr<Node> > boxe
     }
 }
 
-void CrowdNavigation::CreateMovingBarrels()
+void CrowdNavigation::CreateMovingBarrels(DynamicNavigationMesh* navMesh)
 {
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     Node* barrel = scene_->CreateChild("Barrel");
@@ -314,12 +318,12 @@ void CrowdNavigation::CreateMovingBarrels()
     model->SetMaterial(material);
     material->SetTexture(TU_DIFFUSE, cache->GetResource<Texture2D>("Textures/TerrainDetail2.dds"));
     model->SetCastShadows(true);
-    for (unsigned i = 0;  i < NUM_BARRELS; ++i)
+    for (unsigned i = 0;  i < 20; ++i)
     {
         Node* clone = barrel->Clone();
         float size = 0.5f + Random(1.0f);
         clone->SetScale(Vector3(size / 1.5f, size * 2.0f, size / 1.5f));
-        clone->SetPosition(navMesh_->FindNearestPoint(Vector3(Random(80.0f) - 40.0, size * 0.5 , Random(80.0f) - 40.0)));
+        clone->SetPosition(navMesh->FindNearestPoint(Vector3(Random(80.0f) - 40.0, size * 0.5 , Random(80.0f) - 40.0)));
         CrowdAgent* agent = clone->CreateComponent<CrowdAgent>();
         agent->SetRadius(clone->GetScale().x_ * 0.5f);
         agent->SetHeight(size);
@@ -327,49 +331,21 @@ void CrowdNavigation::CreateMovingBarrels()
     barrel->Remove();
 }
 
-void CrowdNavigation::SetPathPoint()
+void CrowdNavigation::SetPathPoint(bool spawning)
 {
     Vector3 hitPos;
     Drawable* hitDrawable;
 
     if (Raycast(250.0f, hitPos, hitDrawable))
     {
-        Vector3 pathPos = navMesh_->FindNearestPoint(hitPos, Vector3(1.0f, 1.0f, 1.0f));
-
-        if (GetSubsystem<Input>()->GetQualifierDown(QUAL_SHIFT))
+        DynamicNavigationMesh* navMesh = scene_->GetComponent<DynamicNavigationMesh>();
+        Vector3 pathPos = navMesh->FindNearestPoint(hitPos, Vector3(1.0f, 1.0f, 1.0f));
+        if (spawning)
             // Spawn a jack at the target position
             SpawnJack(pathPos);
-        else if (GetSubsystem<Input>()->GetQualifierDown(QUAL_CTRL) && agents_.Size() > NUM_BARRELS)
-        {
-            // Teleport
-            CrowdAgent* agent = agents_[NUM_BARRELS]; // Get first Jack agent
-            Node* node = agent->GetNode();
-            if (node->GetName() == "Barrel")
-                return;
-            node->LookAt(pathPos); // Face target
-            agent->SetMoveVelocity(Vector3(0.0f, 0.0f, 0.0f)); // Stop agent
-            node->SetPosition(pathPos);
-        }
         else
-        {
-            // Set target position and init agents' move
-            for (unsigned i = NUM_BARRELS; i < agents_.Size(); ++i)
-            {
-                CrowdAgent* agent = agents_[i];
-                if (i == NUM_BARRELS)
-                {
-                    // The first Jack agent will always move to the exact position and is strong enough to push barrels and his siblings (no avoidance)
-                    agent->SetNavigationPushiness(PUSHINESS_HIGH);
-                    agent->SetMoveTarget(pathPos);
-                }
-                else
-                {
-                    // Other Jack agents will move to a random point nearby
-                    Vector3 targetPos = navMesh_->FindNearestPoint(pathPos + Vector3(Random(-4.5f, 4.5f), 0.0f, Random(-4.5f, 4.5f)), Vector3(1.0f, 1.0f, 1.0f));
-                    agent->SetMoveTarget(targetPos);
-                }
-            }
-        }
+            // Set crowd agents target position
+            scene_->GetComponent<DetourCrowdManager>()->SetCrowdTarget(pathPos);
     }
 }
 
@@ -387,10 +363,7 @@ void CrowdNavigation::AddOrRemoveObject()
         if (hitNode->GetName() == "Mushroom")
             hitNode->Remove();
         else if (hitNode->GetName() == "Jack")
-        {
             hitNode->Remove();
-            agents_ = crowdManager_->GetActiveAgents(); // Update agents container
-        }
         else
             CreateMushroom(hitPos);
     }
@@ -465,9 +438,9 @@ void CrowdNavigation::MoveCamera(float timeStep)
 
     // Set destination or spawn a new jack with left mouse button
     if (input->GetMouseButtonPress(MOUSEB_LEFT))
-        SetPathPoint();
+        SetPathPoint(input->GetQualifierDown(QUAL_SHIFT));
     // Add new obstacle or remove existing obstacle/agent with middle mouse button
-    if (input->GetMouseButtonPress(MOUSEB_MIDDLE))
+    else if (input->GetMouseButtonPress(MOUSEB_MIDDLE))
         AddOrRemoveObject();
 
     // Check for loading/saving the scene from/to the file Data/Scenes/CrowdNavigation.xml relative to the executable directory
@@ -476,20 +449,22 @@ void CrowdNavigation::MoveCamera(float timeStep)
         File saveFile(context_, GetSubsystem<FileSystem>()->GetProgramDir() + "Data/Scenes/CrowdNavigation.xml", FILE_WRITE);
         scene_->SaveXML(saveFile);
     }
-    if (input->GetKeyPress(KEY_F7))
+    else if (input->GetKeyPress(KEY_F7))
     {
         File loadFile(context_, GetSubsystem<FileSystem>()->GetProgramDir() + "Data/Scenes/CrowdNavigation.xml", FILE_READ);
         scene_->LoadXML(loadFile);
-
-        // After reload, reacquire navMesh, crowd manager & agents
-        navMesh_ = scene_->GetComponent<DynamicNavigationMesh>();
-        crowdManager_ = scene_->GetComponent<DetourCrowdManager>();
-        agents_ = crowdManager_->GetActiveAgents();
     }
 
     // Toggle debug geometry with space
-    if (input->GetKeyPress(KEY_SPACE))
+    else if (input->GetKeyPress(KEY_SPACE))
         drawDebug_ = !drawDebug_;
+
+    // Toggle instruction text with F12
+    else if (input->GetKeyPress(KEY_F12))
+    {
+        UIElement* instruction = ui->GetRoot()->GetChild(INSTRUCTION);
+        instruction->SetVisible(!instruction->IsVisible());
+    }
 }
 
 void CrowdNavigation::HandleUpdate(StringHash eventType, VariantMap& eventData)
@@ -501,24 +476,6 @@ void CrowdNavigation::HandleUpdate(StringHash eventType, VariantMap& eventData)
 
     // Move the camera, scale movement with time step
     MoveCamera(timeStep);
-
-    // Make the Jack CrowdAgents face the direction of their velocity and update animation
-    for (unsigned i = NUM_BARRELS; i < agents_.Size(); ++i)
-    {
-        CrowdAgent* agent = agents_[i];
-        Node* node = agent->GetNode();
-        AnimationController* animCtrl = node->GetComponent<AnimationController>();
-        Vector3 velocity = agent->GetActualVelocity();
-
-        if (velocity.Length() < 0.6)
-            animCtrl->Stop("Models/Jack_Walk.ani", 0.2);
-        else
-        {
-            node->SetWorldDirection(velocity);
-            animCtrl->PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.2);
-            animCtrl->SetSpeed("Models/Jack_Walk.ani", velocity.Length() * 0.3);
-        }
-    }
 }
 
 void CrowdNavigation::HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
@@ -526,9 +483,9 @@ void CrowdNavigation::HandlePostRenderUpdate(StringHash eventType, VariantMap& e
     if (drawDebug_)
     {
         // Visualize navigation mesh, obstacles and off-mesh connections
-        navMesh_->DrawDebugGeometry(true);
+        scene_->GetComponent<DynamicNavigationMesh>()->DrawDebugGeometry(true);
         // Visualize agents' path and position to reach
-        crowdManager_->DrawDebugGeometry(true);
+        scene_->GetComponent<DetourCrowdManager>()->DrawDebugGeometry(true);
     }
 }
 
@@ -543,8 +500,37 @@ void CrowdNavigation::HandleCrowdAgentFailure(StringHash eventType, VariantMap&
     if (agentState == CROWD_AGENT_INVALID)
     {
         // Get a point on the navmesh using more generous extents
-        Vector3 newPos = navMesh_->FindNearestPoint(node->GetPosition(), Vector3(5.0f, 5.0f, 5.0f));
+        Vector3 newPos = scene_->GetComponent<DynamicNavigationMesh>()->FindNearestPoint(node->GetPosition(), Vector3(5.0f, 5.0f, 5.0f));
         // Set the new node position, CrowdAgent component will automatically reset the state of the agent
         node->SetPosition(newPos);
     }
-}
+}
+
+void CrowdNavigation::HandleCrowdAgentReposition(StringHash eventType, VariantMap& eventData)
+{
+    const char* WALKING_ANI = "Models/Jack_Walk.ani";
+
+    using namespace CrowdAgentReposition;
+
+    Node* node = static_cast<Node*>(eventData[P_NODE].GetPtr());
+    CrowdAgent* agent = static_cast<CrowdAgent*>(eventData[P_CROWD_AGENT].GetPtr());
+    Vector3 velocity = eventData[P_VELOCITY].GetVector3();
+
+    // Only Jack agent has animation controller
+    AnimationController* animCtrl = node->GetComponent<AnimationController>();
+    if (animCtrl)
+    {
+        float speed = velocity.Length();
+        if (speed < agent->GetRadius())
+            // If speed is too low then stopping the animation
+            animCtrl->Stop(WALKING_ANI, 0.8f);
+        else
+        {
+            // Face the direction of its velocity
+            node->SetWorldDirection(velocity);
+            animCtrl->Play(WALKING_ANI, 0, true);
+            // Throttle the animation speed based on agent speed ratio (ratio = 1 is full throttle)
+            animCtrl->SetSpeed(WALKING_ANI, speed / agent->GetMaxSpeed());
+        }
+    }
+}

+ 6 - 12
Source/Samples/39_CrowdNavigation/CrowdNavigation.h

@@ -33,8 +33,6 @@ class Scene;
 
 }
 
-const int NUM_BARRELS = 20;
-
 /// CrowdNavigation example.
 /// This sample demonstrates:
 ///     - Generating a dynamic navigation mesh into the scene
@@ -140,8 +138,8 @@ private:
     void SubscribeToEvents();
     /// Read input and moves the camera.
     void MoveCamera(float timeStep);
-    /// Set path start or end point.
-    void SetPathPoint();
+    /// Set crowd agents target or spawn another jack.
+    void SetPathPoint(bool spawning);
     /// Add new obstacle or remove existing obstacle/agent.
     void AddOrRemoveObject();
     /// Create a "Jack" object at position.
@@ -149,9 +147,9 @@ private:
     /// Create a mushroom object at position.
     void CreateMushroom(const Vector3& pos);
     /// Create an off-mesh connection for each box to make it climbable.
-    void CreateBoxOffMeshConnections(Vector< SharedPtr<Node> > boxes);
+    void CreateBoxOffMeshConnections(DynamicNavigationMesh* navMesh, Node* boxGroup);
     /// Create some movable barrels as crowd agents.
-    void CreateMovingBarrels();
+    void CreateMovingBarrels(DynamicNavigationMesh* navMesh);
     /// Utility function to raycast to the cursor position. Return true if hit.
     bool Raycast(float maxDistance, Vector3& hitPos, Drawable*& hitDrawable);
     /// Handle the logic update event.
@@ -160,13 +158,9 @@ private:
     void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData);
     /// Handle problems with crowd agent placement.
     void HandleCrowdAgentFailure(StringHash eventType, VariantMap& eventData);
+    /// Handle crowd agent reposition.
+    void HandleCrowdAgentReposition(StringHash eventType, VariantMap& eventData);
 
-    /// Dynamic navigation mesh.
-    DynamicNavigationMesh* navMesh_;
-    /// Crowd Manager.
-    DetourCrowdManager* crowdManager_;
-    /// Crowd Agents.
-    PODVector<CrowdAgent*> agents_;
     /// Flag for drawing debug geometry.
     bool drawDebug_;
 };

+ 5 - 5
Source/Urho3D/LuaScript/pkgs/Navigation/CrowdAgent.pkg

@@ -6,10 +6,9 @@ enum CrowdTargetState
     CROWD_AGENT_TARGET_FAILED,
     CROWD_AGENT_TARGET_VALID,
     CROWD_AGENT_TARGET_REQUESTING,
-    CROWD_AGENT_TARGET_WAITINGFORPATH,
     CROWD_AGENT_TARGET_WAITINGFORQUEUE,
-    CROWD_AGENT_TARGET_VELOCITY,
-    CROWD_AGENT_TARGET_ARRIVED
+    CROWD_AGENT_TARGET_WAITINGFORPATH,
+    CROWD_AGENT_TARGET_VELOCITY
 };
 
 enum CrowdAgentState
@@ -24,8 +23,8 @@ enum CrowdAgentState
 class CrowdAgent : public Component
 {
     void SetNavigationFilterType(unsigned filterID);
-    bool SetMoveTarget(const Vector3& position);
-    bool SetMoveVelocity(const Vector3& velocity);
+    void SetMoveTarget(const Vector3& position);
+    void SetMoveVelocity(const Vector3& velocity);
     void SetUpdateNodePosition(bool unodepos);
     void SetMaxAccel(float val);
     void SetMaxSpeed(float val);
@@ -45,6 +44,7 @@ class CrowdAgent : public Component
     float GetMaxAccel() const;
     NavigationQuality GetNavigationQuality() const;
     NavigationPushiness GetNavigationPushiness() const;
+    bool HasArrived() const;
     float GetRadius() const;
     float GetHeight() const;
     Vector3 GetPosition() const;

+ 6 - 14
Source/Urho3D/LuaScript/pkgs/Navigation/DetourCrowdManager.pkg

@@ -19,27 +19,19 @@ class DetourCrowdManager : public Component
     void DrawDebugGeometry(bool depthTest);
 
     void SetNavigationMesh(NavigationMesh *navMesh);
-    void SetMaxAgents(unsigned agentCt);
     void SetAreaCost(unsigned filterID, unsigned areaID, float cost);
+    void SetMaxAgents(unsigned agentCt);
+    void SetCrowdTarget(const Vector3& position, int startId = 0, int endId = M_MAX_INT);
+    void ResetCrowdTarget(int startId = 0, int endId = M_MAX_INT);
+    void SetCrowdVelocity(const Vector3& velocity, int startId = 0, int endId = M_MAX_INT);
 
-    NavigationMesh* GetNavigationMesh();
+    NavigationMesh* GetNavigationMesh() const;
     unsigned GetMaxAgents() const;
     float GetAreaCost(unsigned filterID, unsigned areaID) const;
     unsigned GetAgentCount() const;
-    tolua_outside const PODVector<CrowdAgent*>& DetourCrowdManagerGetActiveAgents @ GetActiveAgents();
+    const PODVector<CrowdAgent*>& GetActiveAgents() const;
 
     tolua_property__get_set NavigationMesh* navigationMesh;
     tolua_property__get_set int maxAgents;
     tolua_readonly tolua_property__get_set unsigned agentCount;
 };
-
-${
-
-static const PODVector<CrowdAgent*>& DetourCrowdManagerGetActiveAgents(DetourCrowdManager* crowdManager)
-{
-    static PODVector<CrowdAgent*> result;
-    result = crowdManager->GetActiveAgents();
-    return result;
-}
-
-$}

+ 55 - 52
Source/Urho3D/Navigation/CrowdAgent.cpp

@@ -45,8 +45,8 @@ namespace Urho3D
 extern const char* NAVIGATION_CATEGORY;
 
 static const unsigned DEFAULT_AGENT_NAVIGATION_FILTER_TYPE = 0;
-static const float DEFAULT_AGENT_MAX_SPEED = 5.0f;
-static const float DEFAULT_AGENT_MAX_ACCEL = 3.6f;
+static const float DEFAULT_AGENT_MAX_SPEED = 0.f;
+static const float DEFAULT_AGENT_MAX_ACCEL = 0.f;
 static const NavigationQuality DEFAULT_AGENT_AVOIDANCE_QUALITY = NAVIGATIONQUALITY_HIGH;
 static const NavigationPushiness DEFAULT_AGENT_NAVIGATION_PUSHINESS = PUSHINESS_MEDIUM;
 
@@ -97,6 +97,7 @@ void CrowdAgent::RegisterObject(Context* context)
     ACCESSOR_ATTRIBUTE("Max Speed", GetMaxSpeed, SetMaxSpeed, float, DEFAULT_AGENT_MAX_SPEED, AM_DEFAULT);
     ACCESSOR_ATTRIBUTE("Radius", GetRadius, SetRadius, float, 0.0f, AM_DEFAULT);
     ACCESSOR_ATTRIBUTE("Height", GetHeight, SetHeight, float, 0.0f, AM_DEFAULT);
+    ACCESSOR_ATTRIBUTE("Target Position", GetTargetPosition, SetMoveTarget, Vector3, Vector3::ZERO, AM_DEFAULT);
     ACCESSOR_ATTRIBUTE("Navigation Filter", GetNavigationFilterType, SetNavigationFilterType, unsigned, DEFAULT_AGENT_NAVIGATION_FILTER_TYPE, AM_DEFAULT);
     ENUM_ACCESSOR_ATTRIBUTE("Navigation Pushiness", GetNavigationPushiness, SetNavigationPushiness, NavigationPushiness, crowdAgentPushinessNames, PUSHINESS_LOW, AM_DEFAULT);
     ENUM_ACCESSOR_ATTRIBUTE("Navigation Quality", GetNavigationQuality, SetNavigationQuality, NavigationQuality, crowdAgentAvoidanceQualityNames, NAVIGATIONQUALITY_LOW, AM_DEFAULT);
@@ -154,7 +155,7 @@ void CrowdAgent::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
 
         debug->AddLine(pos, pos + vel, Color::GREEN, depthTest);
         debug->AddLine(pos + agentHeightVec, pos + desiredVel + agentHeightVec, Color::RED, depthTest);
-        debug->AddCylinder(pos, radius_, height_, Color::GREEN, depthTest);
+        debug->AddCylinder(pos, radius_, height_, HasArrived() ? Color::WHITE : Color::GREEN, depthTest);
     }
 }
 
@@ -175,8 +176,6 @@ void CrowdAgent::AddAgentToCrowd()
             LOGERROR("AddAgentToCrowd: Could not add agent to crowd");
             return;
         }
-        dtCrowdAgentParams& params = crowdManager_->GetCrowd()->getEditableAgent(agentCrowdId_)->params;
-        params.userData = this;
         crowdManager_->UpdateAgentNavigationQuality(this, navQuality_);
         crowdManager_->UpdateAgentPushiness(this, navPushiness_);
         previousAgentState_ = GetAgentState();
@@ -198,6 +197,9 @@ void CrowdAgent::AddAgentToCrowd()
             previousAgentState_ = GetAgentState();
             previousTargetState_ = GetTargetState();
         }
+
+        // Save the initial position to prevent CrowdAgentReposition event being triggered unnecessarily
+        previousPosition_ = GetPosition();
     }
 }
 
@@ -224,23 +226,32 @@ void CrowdAgent::SetNavigationFilterType(unsigned filterType)
     }
 }
 
-bool CrowdAgent::SetMoveTarget(const Vector3& position)
+void CrowdAgent::SetMoveTarget(const Vector3& position)
 {
-    if (crowdManager_ && !inCrowd_)
-        AddAgentToCrowd();
-    if (crowdManager_ && inCrowd_)
-    {
+    if (crowdManager_) {
+        if (!inCrowd_)
+            AddAgentToCrowd();
         targetPosition_ = position;
         if (crowdManager_->SetAgentTarget(this, position, targetRef_))
+            MarkNetworkUpdate();
+    }
+}
+
+void CrowdAgent::ResetMoveTarget()
+{
+    if (crowdManager_ && inCrowd_)
+    {
+        const dtCrowdAgent* agent = crowdManager_->GetCrowdAgent(agentCrowdId_);
+        if (agent && agent->active)
         {
+            targetPosition_ = Vector3::ZERO;
+            crowdManager_->GetCrowd()->resetMoveTarget(agentCrowdId_);
             MarkNetworkUpdate();
-            return true;
         }
     }
-    return false;
 }
 
-bool CrowdAgent::SetMoveVelocity(const Vector3& velocity)
+void CrowdAgent::SetMoveVelocity(const Vector3& velocity)
 {
     if (crowdManager_ && inCrowd_)
     {
@@ -251,7 +262,6 @@ bool CrowdAgent::SetMoveVelocity(const Vector3& velocity)
             MarkNetworkUpdate();
         }
     }
-    return false;
 }
 
 void CrowdAgent::SetMaxSpeed(float speed)
@@ -369,30 +379,17 @@ Urho3D::CrowdAgentState CrowdAgent::GetAgentState() const
 
 Urho3D::CrowdTargetState CrowdAgent::GetTargetState() const
 {
-    if (crowdManager_ && inCrowd_)
-    {
-        const dtCrowdAgent* agent = crowdManager_->GetCrowdAgent(agentCrowdId_);
-        if (!agent || !agent->active)
-            return CROWD_AGENT_TARGET_NONE;
+    const dtCrowdAgent* agent = crowdManager_ && inCrowd_ ? crowdManager_->GetCrowdAgent(agentCrowdId_) : 0;
+    return agent && agent->active ? (CrowdTargetState)agent->targetState : CROWD_AGENT_TARGET_NONE;
+}
 
-        // Determine if we've arrived at the target
-        if (agent->targetState == DT_CROWDAGENT_TARGET_VALID)
-        {
-            if (agent->ncorners)
-            {
-                // Is the agent at the end of its path?
-                if (agent->cornerFlags[agent->ncorners - 1] & DT_STRAIGHTPATH_END)
-                {
-                    // Within its own radius of the goal?
-                    if (dtVdist2D(agent->npos, &agent->cornerVerts[(agent->ncorners - 1) * 3]) <= agent->params.radius)
-                        return CROWD_AGENT_TARGET_ARRIVED;
-            
-                }
-            }
-        }
-        return (CrowdTargetState)agent->targetState;
-    }
-    return CROWD_AGENT_TARGET_NONE;
+bool CrowdAgent::HasArrived() const
+{
+    // Is the agent at the end of its path and within its own radius of the goal?
+    const dtCrowdAgent* agent = crowdManager_->GetCrowdAgent(agentCrowdId_);
+    return agent && agent->active && (!agent->ncorners ||
+        (agent->cornerFlags[agent->ncorners - 1] & DT_STRAIGHTPATH_END &&
+            Equals(dtVdist2D(agent->npos, &agent->cornerVerts[(agent->ncorners - 1) * 3]), 0.f)));
 }
 
 void CrowdAgent::SetUpdateNodePosition(bool unodepos)
@@ -401,23 +398,29 @@ void CrowdAgent::SetUpdateNodePosition(bool unodepos)
     MarkNetworkUpdate();
 }
 
-void CrowdAgent::OnCrowdAgentReposition(const Vector3& newPos, const Vector3& newDirection)
+void CrowdAgent::OnCrowdAgentReposition(const Vector3& newPos, const Vector3& newVel)
 {
     if (node_)
     {
         // Notify parent node of the reposition
-        VariantMap& map = GetContext()->GetEventDataMap();
-        map[CrowdAgentReposition::P_NODE] = GetNode();
-        map[CrowdAgentReposition::P_CROWD_AGENT] = this;
-        map[CrowdAgentReposition::P_POSITION] = newPos;
-        map[CrowdAgentReposition::P_VELOCITY] = GetActualVelocity();
-        SendEvent(E_CROWD_AGENT_REPOSITION, map);
-        
-        if (updateNodePosition_)
+        if (newPos != previousPosition_)
         {
-            ignoreTransformChanges_ = true;
-            node_->SetPosition(newPos);
-            ignoreTransformChanges_ = false;
+            previousPosition_ = newPos;
+
+            VariantMap& map = GetContext()->GetEventDataMap();
+            map[CrowdAgentReposition::P_NODE] = GetNode();
+            map[CrowdAgentReposition::P_CROWD_AGENT] = this;
+            map[CrowdAgentReposition::P_POSITION] = newPos;
+            map[CrowdAgentReposition::P_VELOCITY] = newVel;
+            map[CrowdAgentReposition::P_ARRIVED] = HasArrived();
+            SendEvent(E_CROWD_AGENT_REPOSITION, map);
+
+            if (updateNodePosition_)
+            {
+                ignoreTransformChanges_ = true;
+                node_->SetPosition(newPos);
+                ignoreTransformChanges_ = false;
+            }
         }
 
         // Send a notification event if we've reached the destination
@@ -431,7 +434,7 @@ void CrowdAgent::OnCrowdAgentReposition(const Vector3& newPos, const Vector3& ne
             map[CrowdAgentStateChanged::P_CROWD_TARGET_STATE] = newTargetState;
             map[CrowdAgentStateChanged::P_CROWD_AGENT_STATE] = newAgentState;
             map[CrowdAgentStateChanged::P_POSITION] = newPos;
-            map[CrowdAgentStateChanged::P_VELOCITY] = GetActualVelocity();
+            map[CrowdAgentStateChanged::P_VELOCITY] = newVel;
             SendEvent(E_CROWD_AGENT_STATE_CHANGED, map);
 
             // Send a failure event if either state is a failed status
@@ -443,7 +446,7 @@ void CrowdAgent::OnCrowdAgentReposition(const Vector3& newPos, const Vector3& ne
                 map[CrowdAgentFailure::P_CROWD_TARGET_STATE] = newTargetState;
                 map[CrowdAgentFailure::P_CROWD_AGENT_STATE] = newAgentState;
                 map[CrowdAgentFailure::P_POSITION] = newPos;
-                map[CrowdAgentFailure::P_VELOCITY] = GetActualVelocity();
+                map[CrowdAgentFailure::P_VELOCITY] = newVel;
                 SendEvent(E_CROWD_AGENT_FAILURE, map);
             }
 
@@ -460,7 +463,7 @@ PODVector<unsigned char> CrowdAgent::GetAgentDataAttr() const
         return Variant::emptyBuffer;
     dtCrowd* crowd = crowdManager_->GetCrowd();
     const dtCrowdAgent* agent = crowd->getAgent(agentCrowdId_);
-    
+
     // Reading it back in isn't this simple, see SetAgentDataAttr
     VectorBuffer ret;
     ret.Write(agent, sizeof(dtCrowdAgent));

+ 17 - 12
Source/Urho3D/Navigation/CrowdAgent.h

@@ -34,17 +34,16 @@ enum CrowdTargetState
     CROWD_AGENT_TARGET_FAILED,
     CROWD_AGENT_TARGET_VALID,
     CROWD_AGENT_TARGET_REQUESTING,
-    CROWD_AGENT_TARGET_WAITINGFORPATH,
     CROWD_AGENT_TARGET_WAITINGFORQUEUE,
-    CROWD_AGENT_TARGET_VELOCITY,
-    CROWD_AGENT_TARGET_ARRIVED
+    CROWD_AGENT_TARGET_WAITINGFORPATH,
+    CROWD_AGENT_TARGET_VELOCITY
 };
 
 enum CrowdAgentState
 {
-    CROWD_AGENT_INVALID = 0,	///< The agent is not in a valid state.
-    CROWD_AGENT_READY,			///< The agent is traversing a normal navigation mesh polygon.
-    CROWD_AGENT_TRAVERSINGLINK	///< The agent is traversing an off-mesh connection.
+    CROWD_AGENT_INVALID = 0,      ///< The agent is not in a valid state.
+    CROWD_AGENT_READY,            ///< The agent is traversing a normal navigation mesh polygon.
+    CROWD_AGENT_TRAVERSINGLINK    ///< The agent is traversing an off-mesh connection.
 };
 
 /// DetourCrowd Agent, requires a DetourCrowdManager in the scene. Agent's radius and height is set through the navigation mesh.
@@ -71,9 +70,11 @@ public:
     /// Set the navigation filter type the agent will use.
     void SetNavigationFilterType(unsigned filterTypeID);
     /// Submit a new move request for this agent.
-    bool SetMoveTarget(const Vector3& position);
+    void SetMoveTarget(const Vector3& position);
+    /// Reset any request for the specified agent.
+    void ResetMoveTarget();
     /// Submit a new move velocity request for this agent.
-    bool SetMoveVelocity(const Vector3& velocity);
+    void SetMoveVelocity(const Vector3& velocity);
     /// Update the node position. When set to false, the node position should be updated by other means (e.g. using Physics) in response to the E_CROWD_AGENT_REPOSITION event.
     void SetUpdateNodePosition(bool unodepos);
     /// Set the agent's max acceleration.
@@ -110,7 +111,7 @@ public:
     /// Get the agent's max velocity.
     float GetMaxSpeed() const { return maxSpeed_; }
     /// Get the agent's max acceleration.
-    float GetMaxAccel()	const { return maxAccel_; }
+    float GetMaxAccel() const { return maxAccel_; }
     /// Get the agent's radius.
     float GetRadius() const { return radius_; }
     /// Get the agent's height.
@@ -119,6 +120,8 @@ public:
     NavigationQuality GetNavigationQuality() const {return navQuality_; }
     /// Get the agent's navigation pushiness.
     NavigationPushiness GetNavigationPushiness() const {return navPushiness_; }
+    /// Return true when the agent has arrived at its target.
+    bool HasArrived() const;
 
     /// Get serialized data of internal state.
     PODVector<unsigned char> GetAgentDataAttr() const;
@@ -127,7 +130,7 @@ public:
 
 protected:
     /// Update the nodes position if updateNodePosition is set. Is called in DetourCrowdManager::Update().
-    virtual void OnCrowdAgentReposition(const Vector3& newPos, const Vector3& newDirection);
+    virtual void OnCrowdAgentReposition(const Vector3& newPos, const Vector3& newVel);
     /// Handle node being assigned.
     virtual void OnNodeSet(Node* node);
     /// \todo Handle node transform being dirtied.
@@ -144,9 +147,9 @@ private:
     /// DetourCrowd reference to this agent.
     int agentCrowdId_;
     /// Reference to poly closest to requested target position.
-    unsigned int targetRef_;         
+    unsigned int targetRef_;
     /// Actual target position, closest to that requested.
-    Vector3 targetPosition_;   
+    Vector3 targetPosition_;
     /// Flag indicating the node's position should be updated by Detour crowd manager.
     bool updateNodePosition_;
     /// Agent's max acceleration.
@@ -163,6 +166,8 @@ private:
     NavigationQuality navQuality_;
     /// Agent's Navigation Pushiness.
     NavigationPushiness navPushiness_;
+    /// Agent's previous position used to check for position changes.
+    Vector3 previousPosition_;
     /// Agent's previous target state used to check for state changes.
     CrowdTargetState previousTargetState_;
     /// Agent's previous agent state used to check for state changes.

+ 48 - 13
Source/Urho3D/Navigation/DetourCrowdManager.cpp

@@ -117,9 +117,44 @@ void DetourCrowdManager::SetMaxAgents(unsigned agentCt)
     MarkNetworkUpdate();
 }
 
-NavigationMesh* DetourCrowdManager::GetNavigationMesh()
+void DetourCrowdManager::SetCrowdTarget(const Vector3& position, int startId, int endId)
 {
-    return navigationMesh_.Get();
+    startId = Max(0, startId);
+    endId = Clamp(endId, startId, agents_.Size() - 1);
+    Vector3 moveTarget(position);
+    for (int i = startId; i <= endId; ++i)
+    {
+        // Skip agent that does not have acceleration
+        if (agents_[i]->GetMaxAccel() > 0.f)
+        {
+            agents_[i]->SetMoveTarget(moveTarget);
+            // FIXME: Should reimplement this using event callback, i.e. it should be application-specific to decide what is the desired crowd formation when they reach the target
+            if (navigationMesh_)
+                moveTarget = navigationMesh_->FindNearestPoint(position + Vector3(Random(-4.5f, 4.5f), 0.0f, Random(-4.5f, 4.5f)), Vector3(1.0f, 1.0f, 1.0f));
+        }
+    }
+}
+
+void DetourCrowdManager::ResetCrowdTarget(int startId, int endId)
+{
+    startId = Max(0, startId);
+    endId = Clamp(endId, startId, agents_.Size() - 1);
+    for (int i = startId; i <= endId; ++i)
+    {
+        if (agents_[i]->GetMaxAccel() > 0.f)
+            agents_[i]->ResetMoveTarget();
+    }
+}
+
+void DetourCrowdManager::SetCrowdVelocity(const Vector3& velocity, int startId, int endId)
+{
+    startId = Max(0, startId);
+    endId = Clamp(endId, startId, agents_.Size() - 1);
+    for (int i = startId; i <= endId; ++i)
+    {
+        if (agents_[i]->GetMaxAccel() > 0.f)
+            agents_[i]->SetMoveVelocity(velocity);
+    }
 }
 
 float DetourCrowdManager::GetAreaCost(unsigned filterID, unsigned areaID) const
@@ -149,9 +184,17 @@ void DetourCrowdManager::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
             if (!ag->active)
                 continue;
 
+            // Draw CrowdAgent shape (from its radius & height)
+            CrowdAgent* crowdAgent = static_cast<CrowdAgent*>(ag->params.userData);
+            crowdAgent->DrawDebugGeometry(debug, depthTest);
+
+            // Draw move target if any
+            if (crowdAgent->GetTargetState() == CROWD_AGENT_TARGET_NONE)
+                continue;
+
             Color color(0.6f, 0.2f, 0.2f, 1.0f);
 
-            // Render line to target:
+            // Draw line to target
             Vector3 pos1(ag->npos[0], ag->npos[1], ag->npos[2]);
             Vector3 pos2;
             for (int i = 0; i < ag->ncorners; ++i)
@@ -167,12 +210,8 @@ void DetourCrowdManager::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
             pos2.z_ = ag->targetPos[2];
             debug->AddLine(pos1, pos2, color, depthTest);
 
-            // Target circle
+            // Draw target circle
             debug->AddSphere(Sphere(pos2, 0.5f), color, depthTest);
-
-            // Draw CrowdAgent shape (from its radius & height)
-            CrowdAgent* crowdAgent = static_cast<CrowdAgent*>(ag->params.userData);
-            crowdAgent->DrawDebugGeometry(debug, depthTest);
         }
     }
 }
@@ -246,6 +285,7 @@ int DetourCrowdManager::AddAgent(CrowdAgent* agent, const Vector3& pos)
     if (!crowd_ || navigationMesh_.Expired())
         return -1;
     dtCrowdAgentParams params;
+    params.userData = agent;
     if (agent->radius_ <= 0.0f)
         agent->radius_ = navigationMesh_->GetAgentRadius();
     params.radius = agent->radius_;
@@ -444,11 +484,6 @@ const dtCrowdAgent* DetourCrowdManager::GetCrowdAgent(int agent)
     return crowd_ ? crowd_->getAgent(agent) : 0;
 }
 
-dtCrowd* DetourCrowdManager::GetCrowd()
-{
-    return crowd_;
-}
-
 void DetourCrowdManager::HandleSceneSubsystemUpdate(StringHash eventType, VariantMap& eventData)
 {
     using namespace SceneSubsystemUpdate;

+ 10 - 4
Source/Urho3D/Navigation/DetourCrowdManager.h

@@ -43,7 +43,7 @@ enum NavigationQuality
 
 enum NavigationPushiness
 {
-    PUSHINESS_LOW,
+    PUSHINESS_LOW = 0,
     PUSHINESS_MEDIUM,
     PUSHINESS_HIGH
 };
@@ -69,9 +69,15 @@ public:
     void SetAreaCost(unsigned filterTypeID, unsigned areaID, float weight);
     /// Set the maximum number of agents.
     void SetMaxAgents(unsigned agentCt);
+    /// Set the crowd move target. The move target is applied to all crowd agents within the id range, excluding crowd agent which does not have acceleration.
+    void SetCrowdTarget(const Vector3& position, int startId = 0, int endId = M_MAX_INT);
+    /// Reset the crowd move target to all crowd agents within the id range, excluding crowd agent which does not have acceleration.
+    void ResetCrowdTarget(int startId = 0, int endId = M_MAX_INT);
+    /// Set the crowd move velocity. The move velocity is applied to all crowd agents within the id range, excluding crowd agent which does not have acceleration.
+    void SetCrowdVelocity(const Vector3& velocity, int startId = 0, int endId = M_MAX_INT);
 
     /// Get the Navigation mesh assigned to the crowd.
-    NavigationMesh* GetNavigationMesh();
+    NavigationMesh* GetNavigationMesh() const { return navigationMesh_; }
     /// Get the cost of an area-type for the specified navigation filter type.
     float GetAreaCost(unsigned filterTypeID, unsigned areaID) const;
     /// Get the maximum number of agents.
@@ -84,7 +90,7 @@ public:
     /// Add debug geometry to the debug renderer.
     void DrawDebugGeometry(bool depthTest);
     /// Get the currently included agents.
-    PODVector<CrowdAgent*> GetActiveAgents() const { return agents_; }
+    const PODVector<CrowdAgent*>& GetActiveAgents() const { return agents_; }
     /// Create detour crowd component for the specified navigation mesh.
     bool CreateCrowd();
 
@@ -115,7 +121,7 @@ protected:
     /// Get the detour crowd agent.
     const dtCrowdAgent* GetCrowdAgent(int agent);
     /// Get the internal detour crowd component.
-    dtCrowd* GetCrowd();
+    dtCrowd* GetCrowd() const { return crowd_; }
 
 private:
     /// Handle the scene subsystem update event.

+ 1 - 0
Source/Urho3D/Navigation/NavigationEvents.h

@@ -50,6 +50,7 @@ EVENT(E_CROWD_AGENT_REPOSITION, CrowdAgentReposition)
     PARAM(P_CROWD_AGENT, CrowdAgent); // CrowdAgent pointer
     PARAM(P_POSITION, Position); // Vector3
     PARAM(P_VELOCITY, Velocity); // Vector3
+    PARAM(P_ARRIVED, Arrived); // bool
 }
 
 /// Crowd agent's internal state has become invalidated.

+ 6 - 6
Source/Urho3D/Navigation/OffMeshConnection.cpp

@@ -41,9 +41,9 @@ OffMeshConnection::OffMeshConnection(Context* context) :
     endPointID_(0),
     radius_(DEFAULT_RADIUS),
     bidirectional_(true),
+    endPointDirty_(false),
     mask_(DEFAULT_MASK_FLAG),
-    areaId_(DEFAULT_AREA),
-    endPointDirty_(false)
+    areaId_(DEFAULT_AREA)
 {
 }
 
@@ -54,7 +54,7 @@ OffMeshConnection::~OffMeshConnection()
 void OffMeshConnection::RegisterObject(Context* context)
 {
     context->RegisterFactory<OffMeshConnection>(NAVIGATION_CATEGORY);
-    
+
     ACCESSOR_ATTRIBUTE("Is Enabled", IsEnabled, SetEnabled, bool, true, AM_DEFAULT);
     ATTRIBUTE("Endpoint NodeID", int, endPointID_, 0, AM_DEFAULT | AM_NODEID);
     ATTRIBUTE("Radius", float, radius_, DEFAULT_RADIUS, AM_DEFAULT);
@@ -66,7 +66,7 @@ void OffMeshConnection::RegisterObject(Context* context)
 void OffMeshConnection::OnSetAttribute(const AttributeInfo& attr, const Variant& src)
 {
     Serializable::OnSetAttribute(attr, src);
-    
+
     if (attr.offset_ == offsetof(OffMeshConnection, endPointID_))
         endPointDirty_ = true;
 }
@@ -85,10 +85,10 @@ void OffMeshConnection::DrawDebugGeometry(DebugRenderer* debug, bool depthTest)
 {
     if (!node_ || !endPoint_)
         return;
-    
+
     Vector3 start = node_->GetWorldPosition();
     Vector3 end = endPoint_->GetWorldPosition();
-    
+
     debug->AddSphere(Sphere(start, radius_), Color::WHITE, depthTest);
     debug->AddSphere(Sphere(end, radius_), Color::WHITE, depthTest);
     debug->AddLine(start, end, Color::WHITE, depthTest);

+ 7 - 4
Source/Urho3D/Script/NavigationAPI.cpp

@@ -57,7 +57,7 @@ static CScriptArray* DynamicNavigationMeshFindPath(const Vector3& start, const V
 
 static CScriptArray* DetourCrowdManagerGetActiveAgents(DetourCrowdManager* crowd)
 {
-    PODVector<CrowdAgent*> agents = crowd->GetActiveAgents();
+    const PODVector<CrowdAgent*>& agents = crowd->GetActiveAgents();
     return VectorToHandleArray<CrowdAgent>(agents, "Array<CrowdAgent@>");
 }
 
@@ -185,6 +185,9 @@ void RegisterDetourCrowdManager(asIScriptEngine* engine)
     engine->RegisterObjectMethod("DetourCrowdManager", "Array<CrowdAgent@>@ GetActiveAgents()", asFUNCTION(DetourCrowdManagerGetActiveAgents), asCALL_CDECL_OBJLAST);
     engine->RegisterObjectMethod("DetourCrowdManager", "void SetAreaCost(uint, uint, float)", asMETHOD(DetourCrowdManager, SetAreaCost), asCALL_THISCALL);
     engine->RegisterObjectMethod("DetourCrowdManager", "float GetAreaCost(uint, uint)", asMETHOD(DetourCrowdManager, GetAreaCost), asCALL_THISCALL);
+    engine->RegisterObjectMethod("DetourCrowdManager", "void SetCrowdTarget(const Vector3&in, int startId = 0, int endId = M_MAX_INT)", asMETHOD(DetourCrowdManager, SetCrowdTarget), asCALL_THISCALL);
+    engine->RegisterObjectMethod("DetourCrowdManager", "void ResetCrowdTarget(int startId = 0, int endId = M_MAX_INT)", asMETHOD(DetourCrowdManager, ResetCrowdTarget), asCALL_THISCALL);
+    engine->RegisterObjectMethod("DetourCrowdManager", "void SetCrowdVelocity(const Vector3&in, int startId = 0, int endId = M_MAX_INT)", asMETHOD(DetourCrowdManager, SetCrowdVelocity), asCALL_THISCALL);
 }
 
 void RegisterCrowdAgent(asIScriptEngine* engine)
@@ -197,7 +200,6 @@ void RegisterCrowdAgent(asIScriptEngine* engine)
     engine->RegisterEnumValue("CrowdTargetState", "CROWD_AGENT_TARGET_WAITINGFORPATH", CROWD_AGENT_TARGET_WAITINGFORPATH);
     engine->RegisterEnumValue("CrowdTargetState", "CROWD_AGENT_TARGET_WAITINGFORQUEUE", CROWD_AGENT_TARGET_WAITINGFORQUEUE);
     engine->RegisterEnumValue("CrowdTargetState", "CROWD_AGENT_TARGET_VELOCITY", CROWD_AGENT_TARGET_VELOCITY);
-    engine->RegisterEnumValue("CrowdTargetState", "CROWD_AGENT_TARGET_ARRIVED", CROWD_AGENT_TARGET_ARRIVED);
 
     engine->RegisterEnum("CrowdAgentState");
     engine->RegisterEnumValue("CrowdAgentState", "CROWD_AGENT_INVALID", CROWD_AGENT_INVALID);
@@ -218,8 +220,8 @@ void RegisterCrowdAgent(asIScriptEngine* engine)
     engine->RegisterObjectMethod("CrowdAgent", "void DrawDebugGeometry(bool)", asMETHODPR(CrowdAgent, DrawDebugGeometry, (bool), void), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "uint get_navigationFilterType()", asMETHOD(CrowdAgent, GetNavigationFilterType), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "void set_navigationFilterType(uint)", asMETHOD(CrowdAgent, SetNavigationFilterType), asCALL_THISCALL);
-    engine->RegisterObjectMethod("CrowdAgent", "bool SetMoveTarget(const Vector3&in)", asMETHOD(CrowdAgent, SetMoveTarget), asCALL_THISCALL);
-    engine->RegisterObjectMethod("CrowdAgent", "bool SetMoveVelocity(const Vector3&in)", asMETHOD(CrowdAgent, SetMoveVelocity), asCALL_THISCALL);
+    engine->RegisterObjectMethod("CrowdAgent", "void SetMoveTarget(const Vector3&in)", asMETHOD(CrowdAgent, SetMoveTarget), asCALL_THISCALL);
+    engine->RegisterObjectMethod("CrowdAgent", "void SetMoveVelocity(const Vector3&in)", asMETHOD(CrowdAgent, SetMoveVelocity), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "void set_updateNodePosition(bool)", asMETHOD(CrowdAgent, SetUpdateNodePosition), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "bool get_updateNodePosition() const", asMETHOD(CrowdAgent, GetUpdateNodePosition), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "void set_maxAccel(float)", asMETHOD(CrowdAgent, SetMaxAccel), asCALL_THISCALL);
@@ -230,6 +232,7 @@ void RegisterCrowdAgent(asIScriptEngine* engine)
     engine->RegisterObjectMethod("CrowdAgent", "NavigationAvoidanceQuality get_navigationQuality()", asMETHOD(CrowdAgent, GetNavigationQuality), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "void set_navigationPushiness(NavigationPushiness)", asMETHOD(CrowdAgent, SetNavigationPushiness), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "NavigationPushiness get_navigationPushiness()", asMETHOD(CrowdAgent, GetNavigationPushiness), asCALL_THISCALL);
+    engine->RegisterObjectMethod("CrowdAgent", "bool get_arrived() const", asMETHOD(CrowdAgent, HasArrived), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "Vector3 get_desiredVelocity() const", asMETHOD(CrowdAgent, GetDesiredVelocity), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "Vector3 get_actualVelocity() const", asMETHOD(CrowdAgent, GetActualVelocity), asCALL_THISCALL);
     engine->RegisterObjectMethod("CrowdAgent", "Vector3 get_targetPosition() const", asMETHOD(CrowdAgent, GetTargetPosition), asCALL_THISCALL);

+ 67 - 79
bin/Data/LuaScripts/39_CrowdNavigation.lua

@@ -11,10 +11,7 @@
 
 require "LuaScripts/Utilities/Sample"
 
-local navMesh = nil
-local crowdManager = nil
-local agents = {}
-local NUM_BARRELS = 20
+local INSTRUCTION = "instructionText"
 
 function Start()
     -- Execute the common startup for samples
@@ -68,9 +65,9 @@ function CreateScene()
 
     -- Create randomly sized boxes. If boxes are big enough, make them occluders. Occluders will be software rasterized before
     -- rendering to a low-resolution depth-only buffer to test the objects in the view frustum for visibility
-    local boxes = {}
+    local boxGroup = scene_:CreateChild("Boxes")
     for i = 1, 20 do
-        local boxNode = scene_:CreateChild("Box")
+        local boxNode = boxGroup:CreateChild("Box")
         local size = 1.0 + Random(10.0)
         boxNode.position = Vector3(Random(80.0) - 40.0, size * 0.5, Random(80.0) - 40.0)
         boxNode:SetScale(size)
@@ -80,12 +77,11 @@ function CreateScene()
         boxObject.castShadows = true
         if size >= 3.0 then
             boxObject.occluder = true
-            table.insert(boxes, boxNode)
         end
     end
 
     -- Create a DynamicNavigationMesh component to the scene root
-    navMesh = scene_:CreateComponent("DynamicNavigationMesh")
+    local navMesh = scene_:CreateComponent("DynamicNavigationMesh")
     -- Enable drawing debug geometry for obstacles and off-mesh connections
     navMesh.drawObstacles = true
     navMesh.drawOffMeshConnections = true
@@ -107,7 +103,7 @@ function CreateScene()
     -- Create an off-mesh connection for each box to make it climbable (tiny boxes are skipped).
     -- Note that OffMeshConnections must be added before building the navMesh, but as we are adding Obstacles next, tiles will be automatically rebuilt.
     -- Creating connections post-build here allows us to use FindNearestPoint() to procedurally set accurate positions for the connection
-    CreateBoxOffMeshConnections(boxes)
+    CreateBoxOffMeshConnections(navMesh, boxGroup)
 
     -- Create some mushrooms as obstacles. Note that obstacles are non-walkable areas
     for i = 1, 100 do
@@ -115,10 +111,10 @@ function CreateScene()
     end
 
     -- Create a DetourCrowdManager component to the scene root (mandatory for crowd agents)
-    crowdManager = scene_:CreateComponent("DetourCrowdManager")
+    scene_:CreateComponent("DetourCrowdManager")
 
     -- Create some movable barrels. We create them as crowd agents, as for moving entities it is less expensive and more convenient than using obstacles
-    CreateMovingBarrels()
+    CreateMovingBarrels(navMesh)
 
     -- Create Jack node as crowd agent
     SpawnJack(Vector3(-5, 0, 20))
@@ -129,8 +125,10 @@ function CreateScene()
     local camera = cameraNode:CreateComponent("Camera")
     camera.farClip = 300.0
 
-    -- Set an initial position for the camera scene node above the plane
-    cameraNode.position = Vector3(0.0, 5.0, 0.0)
+    -- Set an initial position for the camera scene node above the plane and looking down
+    cameraNode.position = Vector3(0.0, 50.0, 0.0)
+    pitch = 80.0
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0)
 end
 
 function CreateUI()
@@ -144,14 +142,15 @@ function CreateUI()
     cursor:SetPosition(graphics.width / 2, graphics.height / 2)
 
     -- Construct new Text object, set string to display and font to use
-    local instructionText = ui.root:CreateChild("Text")
+    local instructionText = ui.root:CreateChild("Text", INSTRUCTION)
     instructionText.text = "Use WASD keys to move, RMB to rotate view\n"..
         "LMB to set destination, SHIFT+LMB to spawn a Jack\n"..
         "CTRL+LMB to teleport main agent\n"..
         "MMB to add obstacles or remove obstacles/agents\n"..
         "F5 to save scene, F7 to load\n"..
-        "Space to toggle debug geometry"
-    instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 12)
+        "Space to toggle debug geometry\n"..
+        "F12 to toggle this instruction text"
+    instructionText:SetFont(cache:GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15)
     -- The text has multiple rows. Center them in relation to each other
     instructionText.textAlignment = HA_CENTER
 
@@ -171,13 +170,15 @@ function SubscribeToEvents()
     -- Subscribe HandleUpdate() function for processing update events
     SubscribeToEvent("Update", "HandleUpdate")
 
-    -- Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request
-    -- debug geometry
+    -- Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request debug geometry
     SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate")
 
     -- Subscribe HandleCrowdAgentFailure() function for resolving invalidation issues with agents, during which we
     -- use a larger extents for finding a point on the navmesh to fix the agent's position
     SubscribeToEvent("CrowdAgentFailure", "HandleCrowdAgentFailure")
+
+    -- Subscribe HandleCrowdAgentReposition() function for controlling the animation
+    SubscribeToEvent("CrowdAgentReposition", "HandleCrowdAgentReposition")
 end
 
 function SpawnJack(pos)
@@ -192,9 +193,8 @@ function SpawnJack(pos)
     -- Create a CrowdAgent component and set its height and realistic max speed/acceleration. Use default radius
     local agent = jackNode:CreateComponent("CrowdAgent")
     agent.height = 2.0
-    agent.maxSpeed = 4.0
+    agent.maxSpeed = 3.0
     agent.maxAccel = 100.0
-    agents = crowdManager:GetActiveAgents() -- Update agents container
 end
 
 function CreateMushroom(pos)
@@ -213,7 +213,8 @@ function CreateMushroom(pos)
     obstacle.height = mushroomNode.scale.y
 end
 
-function CreateBoxOffMeshConnections(boxes)
+function CreateBoxOffMeshConnections(navMesh, boxGroup)
+    boxes = boxGroup:GetChildren()
     for i, box in ipairs(boxes) do
         local boxPos = box.position
         local boxHalfSize = box.scale.x / 2
@@ -230,14 +231,14 @@ function CreateBoxOffMeshConnections(boxes)
     end
 end
 
-function CreateMovingBarrels()
+function CreateMovingBarrels(navMesh)
     local barrel = scene_:CreateChild("Barrel")
     local model = barrel:CreateComponent("StaticModel")
     model.model = cache:GetResource("Model", "Models/Cylinder.mdl")
     model.material = cache:GetResource("Material", "Materials/StoneTiled.xml")
     model.material:SetTexture(TU_DIFFUSE, cache:GetResource("Texture2D", "Textures/TerrainDetail2.dds"))
     model.castShadows = true
-    for i = 1, NUM_BARRELS do
+    for i = 1, 20 do
         local clone = barrel:Clone()
         local size = 0.5 + Random(1)
         clone.scale = Vector3(size / 1.5, size * 2, size / 1.5)
@@ -249,38 +250,19 @@ function CreateMovingBarrels()
     barrel:Remove()
 end
 
-function SetPathPoint()
+function SetPathPoint(spawning)
     local hitPos, hitDrawable = Raycast(250.0)
 
     if hitDrawable then
+        local navMesh = scene_:GetComponent("DynamicNavigationMesh")
         local pathPos = navMesh:FindNearestPoint(hitPos, Vector3.ONE)
 
-        if input:GetQualifierDown(QUAL_SHIFT) then
-            -- Spawn a Jack
+        if spawning then
+            -- Spawn a jack at the target position
             SpawnJack(pathPos)
-
-        elseif input:GetQualifierDown(QUAL_CTRL) and table.maxn(agents) > NUM_BARRELS then
-            -- Teleport
-            local agent = agents[NUM_BARRELS + 1] -- Get first Jack agent
-            local node = agent.node
-            node:LookAt(pathPos) -- Face target
-            agent:SetMoveVelocity(Vector3.ZERO) -- Stop agent
-            node.position = pathPos
-
         else
-            -- Set target position and init agents' move
-            for i = NUM_BARRELS + 1, table.maxn(agents) do
-                local agent = agents[i]
-                if i == NUM_BARRELS + 1 then
-                    -- The first Jack agent will always move to the exact position and is strong enough to push barrels and his siblings (no avoidance)
-                    agent.navigationPushiness = PUSHINESS_HIGH
-                    agent:SetMoveTarget(pathPos)
-                else
-                    -- Other Jack agents will move to a random point nearby
-                    local targetPos = navMesh:FindNearestPoint(pathPos + Vector3(Random(-4.5, 4.5), 0, Random(-4.5, 4.5)), Vector3.ONE)
-                    agent:SetMoveTarget(targetPos)
-                end
-            end
+            -- Set crowd agents target position
+            scene_:GetComponent("DetourCrowdManager"):SetCrowdTarget(pathPos)
         end
     end
 end
@@ -295,7 +277,6 @@ function AddOrRemoveObject()
             hitNode:Remove()
         elseif hitNode.name == "Jack" then
             hitNode:Remove()
-            agents = crowdManager:GetActiveAgents() -- Update agents container
         else
             CreateMushroom(hitPos)
         end
@@ -360,30 +341,29 @@ function MoveCamera(timeStep)
     if input:GetKeyDown(KEY_D) then
         cameraNode:Translate(Vector3(1.0, 0.0, 0.0) * MOVE_SPEED * timeStep)
     end
+
     -- Set destination or spawn a jack with left mouse button
     if input:GetMouseButtonPress(MOUSEB_LEFT) then
-        SetPathPoint()
-    end
+        SetPathPoint(input:GetQualifierDown(QUAL_SHIFT))
     -- Add new obstacle or remove existing obstacle/agent with middle mouse button
-    if input:GetMouseButtonPress(MOUSEB_MIDDLE) then
+    elseif input:GetMouseButtonPress(MOUSEB_MIDDLE) then
         AddOrRemoveObject()
     end
 
     -- Check for loading/saving the scene from/to the file Data/Scenes/CrowdNavigation.xml relative to the executable directory
     if input:GetKeyPress(KEY_F5) then
         scene_:SaveXML(fileSystem:GetProgramDir().."Data/Scenes/CrowdNavigation.xml")
-    end
-    if input:GetKeyPress(KEY_F7) then
+    elseif input:GetKeyPress(KEY_F7) then
         scene_:LoadXML(fileSystem:GetProgramDir().."Data/Scenes/CrowdNavigation.xml")
-        -- After reload, reacquire navMesh, crowd manager & agents
-        navMesh = scene_:GetComponent("DynamicNavigationMesh")
-        crowdManager = scene_:GetComponent("DetourCrowdManager")
-        agents = crowdManager:GetActiveAgents()
-    end
 
     -- Toggle debug geometry with space
-    if input:GetKeyPress(KEY_SPACE) then
+    elseif input:GetKeyPress(KEY_SPACE) then
         drawDebug = not drawDebug
+
+    -- Toggle instruction text with F12
+    elseif input:GetKeyPress(KEY_F12) then
+        instruction = ui.root:GetChild(INSTRUCTION)
+        instruction.visible = not instruction.visible
     end
 end
 
@@ -393,30 +373,14 @@ function HandleUpdate(eventType, eventData)
 
     -- Move the camera, scale movement with time step
     MoveCamera(timeStep)
-
-    -- Make the Jack CrowdAgents face the direction of their velocity and update animation
-    for i = NUM_BARRELS + 1, table.maxn(agents) do
-        local agent = agents[i]
-        local node = agent.node
-        local animCtrl = node:GetComponent("AnimationController")
-        local velocity = agent.actualVelocity
-
-        if velocity:Length() < 0.6 then
-            animCtrl:Stop("Models/Jack_Walk.ani", 0.2)
-        else
-            node.worldDirection = velocity
-            animCtrl:PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.2)
-            animCtrl:SetSpeed("Models/Jack_Walk.ani", velocity:Length() * 0.3)
-        end
-    end
 end
 
 function HandlePostRenderUpdate(eventType, eventData)
     if drawDebug then
         -- Visualize navigation mesh, obstacles and off-mesh connections
-        navMesh:DrawDebugGeometry(true)
+        scene_:GetComponent("DynamicNavigationMesh"):DrawDebugGeometry(true)
         -- Visualize agents' path and position to reach
-        crowdManager:DrawDebugGeometry(true)
+        scene_:GetComponent("DetourCrowdManager"):DrawDebugGeometry(true)
     end
 end
 
@@ -427,12 +391,36 @@ function HandleCrowdAgentFailure(eventType, eventData)
     -- If the agent's state is invalid, likely from spawning on the side of a box, find a point in a larger area
     if agentState == CROWD_AGENT_INVALID then
         -- Get a point on the navmesh using more generous extents
-        local newPos = navMesh:FindNearestPoint(node.position, Vector3(5, 5, 5))
+        local newPos = scene_:GetComponent("DynamicNavigationMesh"):FindNearestPoint(node.position, Vector3(5, 5, 5))
         -- Set the new node position, CrowdAgent component will automatically reset the state of the agent
         node.position = newPos
     end
 end
 
+function HandleCrowdAgentReposition(eventType, eventData)
+    local WALKING_ANI = "Models/Jack_Walk.ani"
+
+    local node = eventData:GetPtr("Node", "Node")
+    local agent = eventData:GetPtr("CrowdAgent", "CrowdAgent")
+    local velocity = eventData:GetVector3("Velocity")
+
+    -- Only Jack agent has animation controller
+    local animCtrl = node:GetComponent("AnimationController")
+    if animCtrl ~= nil then
+        local speed = velocity:Length()
+        if speed < agent.radius then
+            -- If speed is too low then stopping the animation
+            animCtrl:Stop(WALKING_ANI, 0.8)
+        else
+            -- Face the direction of its velocity
+            node.worldDirection = velocity
+            animCtrl:Play(WALKING_ANI, 0, true)
+            -- Throttle the animation speed based on agent speed ratio (ratio = 1 is full throttle)
+            animCtrl:SetSpeed(WALKING_ANI, speed / agent.maxSpeed)
+        end
+    end
+end
+
 -- Create XML patch instructions for screen joystick layout specific to this sample app
 function GetScreenJoystickPatchString()
     return

+ 71 - 90
bin/Data/Scripts/39_CrowdNavigation.as

@@ -11,9 +11,7 @@
 
 #include "Scripts/Utilities/Sample.as"
 
-DynamicNavigationMesh@ navMesh;
-DetourCrowdManager@ crowdManager;
-const int NUM_BARRELS = 20;
+const String INSTRUCTION("instructionText");
 
 void Start()
 {
@@ -70,10 +68,10 @@ void CreateScene()
 
     // Create randomly sized boxes. If boxes are big enough, make them occluders. Occluders will be software rasterized before
     // rendering to a low-resolution depth-only buffer to test the objects in the view frustum for visibility
-    Array<Node@> boxes;
+    Node@ boxGroup = scene_.CreateChild("Boxes");
     for (uint i = 0; i < 20; ++i)
     {
-        Node@ boxNode = scene_.CreateChild("Box");
+        Node@ boxNode = boxGroup.CreateChild("Box");
         float size = 1.0f + Random(10.0f);
         boxNode.position = Vector3(Random(80.0f) - 40.0f, size * 0.5f, Random(80.0f) - 40.0f);
         boxNode.SetScale(size);
@@ -82,14 +80,11 @@ void CreateScene()
         boxObject.material = cache.GetResource("Material", "Materials/Stone.xml");
         boxObject.castShadows = true;
         if (size >= 3.0f)
-        {
             boxObject.occluder = true;
-            boxes.Push(boxNode);
-        }
     }
 
     // Create a DynamicNavigationMesh component to the scene root
-    navMesh = scene_.CreateComponent("DynamicNavigationMesh");
+    DynamicNavigationMesh@ navMesh = scene_.CreateComponent("DynamicNavigationMesh");
     // Enable drawing debug geometry for obstacles and off-mesh connections
     navMesh.drawObstacles = true;
     navMesh.drawOffMeshConnections = true;
@@ -111,17 +106,17 @@ void CreateScene()
     // Create an off-mesh connection to each box to make it climbable (tiny boxes are skipped). A connection is built from 2 nodes.
     // Note that OffMeshConnections must be added before building the navMesh, but as we are adding Obstacles next, tiles will be automatically rebuilt.
     // Creating connections post-build here allows us to use FindNearestPoint() to procedurally set accurate positions for the connection
-    CreateBoxOffMeshConnections(boxes);
+    CreateBoxOffMeshConnections(navMesh, boxGroup);
 
     // Create some mushrooms as obstacles. Note that obstacles are non-walkable areas
     for (uint i = 0; i < 100; ++i)
         CreateMushroom(Vector3(Random(90.0f) - 45.0f, 0.0f, Random(90.0f) - 45.0f));
 
     // Create a DetourCrowdManager component to the scene root (mandatory for crowd agents)
-    crowdManager = scene_.CreateComponent("DetourCrowdManager");
+    scene_.CreateComponent("DetourCrowdManager");
 
     // Create some movable barrels. We create them as crowd agents, as for moving entities it is less expensive and more convenient than using obstacles
-    CreateMovingBarrels();
+    CreateMovingBarrels(navMesh);
 
     // Create Jack node as crowd agent
     SpawnJack(Vector3(-5.0f, 0, 20.0f));
@@ -132,8 +127,10 @@ void CreateScene()
     Camera@ camera = cameraNode.CreateComponent("Camera");
     camera.farClip = 300.0f;
 
-    // Set an initial position for the camera scene node above the plane
-    cameraNode.position = Vector3(0.0f, 5.0f, 0.0f);
+    // Set an initial position for the camera scene node above the plane and looking down
+    cameraNode.position = Vector3(0.0f, 50.0f, 0.0f);
+    pitch = 80.0f;
+    cameraNode.rotation = Quaternion(pitch, yaw, 0.0f);
 }
 
 void CreateUI()
@@ -148,15 +145,16 @@ void CreateUI()
     cursor.SetPosition(graphics.width / 2, graphics.height / 2);
 
     // Construct new Text object, set string to display and font to use
-    Text@ instructionText = ui.root.CreateChild("Text");
+    Text@ instructionText = ui.root.CreateChild("Text", INSTRUCTION);
     instructionText.text =
         "Use WASD keys to move, RMB to rotate view\n"
         "LMB to set destination, SHIFT+LMB to spawn a Jack\n"
         "CTRL+LMB to teleport main agent\n"
         "MMB to add obstacles or remove obstacles/agents\n"
         "F5 to save scene, F7 to load\n"
-        "Space to toggle debug geometry";
-    instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 12);
+        "Space to toggle debug geometry\n"
+        "F12 to toggle this instruction text";
+    instructionText.SetFont(cache.GetResource("Font", "Fonts/Anonymous Pro.ttf"), 15);
     // The text has multiple rows. Center them in relation to each other
     instructionText.textAlignment = HA_CENTER;
 
@@ -178,13 +176,15 @@ void SubscribeToEvents()
     // Subscribe HandleUpdate() function for processing update events
     SubscribeToEvent("Update", "HandleUpdate");
 
-    // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request
-    // debug geometry
+    // Subscribe HandlePostRenderUpdate() function for processing the post-render update event, during which we request debug geometry
     SubscribeToEvent("PostRenderUpdate", "HandlePostRenderUpdate");
 
     // Subscribe HandleCrowdAgentFailure() function for resolving invalidation issues with agents, during which we
     // use a larger extents for finding a point on the navmesh to fix the agent's position
     SubscribeToEvent("CrowdAgentFailure", "HandleCrowdAgentFailure");
+
+    // Subscribe HandleCrowdAgentReposition() function for controlling the animation
+    SubscribeToEvent("CrowdAgentReposition", "HandleCrowdAgentReposition");
 }
 
 void CreateMushroom(const Vector3& pos)
@@ -217,12 +217,13 @@ void SpawnJack(const Vector3& pos)
     // Create a CrowdAgent component and set its height and realistic max speed/acceleration. Use default radius
     CrowdAgent@ agent = jackNode.CreateComponent("CrowdAgent");
     agent.height = 2.0f;
-    agent.maxSpeed = 4.0f;
+    agent.maxSpeed = 3.0f;
     agent.maxAccel = 100.0f;
 }
 
-void CreateBoxOffMeshConnections(Array<Node@> boxes)
+void CreateBoxOffMeshConnections(DynamicNavigationMesh@ navMesh, Node@ boxGroup)
 {
+    Array<Node@>@ boxes = boxGroup.GetChildren();
     for (uint i=0; i < boxes.length; ++i)
     {
         Node@ box = boxes[i];
@@ -241,7 +242,7 @@ void CreateBoxOffMeshConnections(Array<Node@> boxes)
     }
 }
 
-void CreateMovingBarrels()
+void CreateMovingBarrels(DynamicNavigationMesh@ navMesh)
 {
     Node@ barrel = scene_.CreateChild("Barrel");
     StaticModel@ model = barrel.CreateComponent("StaticModel");
@@ -250,7 +251,7 @@ void CreateMovingBarrels()
     model.material = material;
     material.textures[TU_DIFFUSE] = cache.GetResource("Texture2D", "Textures/TerrainDetail2.dds");
     model.castShadows = true;
-    for (uint i = 0;  i < NUM_BARRELS; ++i)
+    for (uint i = 0;  i < 20; ++i)
     {
         Node@ clone = barrel.Clone();
         float size = 0.5 + Random(1);
@@ -263,52 +264,21 @@ void CreateMovingBarrels()
     barrel.Remove();
 }
 
-void SetPathPoint()
+void SetPathPoint(bool spawning)
 {
     Vector3 hitPos;
     Drawable@ hitDrawable;
 
     if (Raycast(250.0f, hitPos, hitDrawable))
     {
+        DynamicNavigationMesh@ navMesh = scene_.GetComponent("DynamicNavigationMesh");
         Vector3 pathPos = navMesh.FindNearestPoint(hitPos, Vector3(1.0f, 1.0f, 1.0f));
-
-        if (input.qualifierDown[QUAL_SHIFT])
-            // Spawn a jack
+        if (spawning)
+            // Spawn a jack at the target position
             SpawnJack(pathPos);
-        else if (input.qualifierDown[QUAL_CTRL])
-        {
-            // Teleport
-            Array<CrowdAgent@>@ agents = crowdManager.GetActiveAgents();
-            if (agents.length <= NUM_BARRELS)
-                return;
-            CrowdAgent@ agent = agents[NUM_BARRELS]; // Get first Jack agent
-            Node@ node = agent.node;
-            node.LookAt(pathPos); // Face target
-            agent.SetMoveVelocity(Vector3(0.0, 0.0, 0.0)); // Stop agent
-            node.position = pathPos;
-        }
         else
-        {
-            // Set target position and init agents' move
-            Array<CrowdAgent@>@ agents = crowdManager.GetActiveAgents();
-            for (uint i = NUM_BARRELS; i < agents.length; ++i)
-            {
-                CrowdAgent@ agent = agents[i];
-
-                if (i == NUM_BARRELS)
-                {
-                    // The first Jack agent will always move to the exact position and is strong enough to push barrels and his siblings (no avoidance)
-                    agent.navigationPushiness = PUSHINESS_HIGH;
-                    agent.SetMoveTarget(pathPos);
-                }
-                else
-                {
-                    // Other Jack agents will move to a random point nearby
-                    Vector3 targetPos = navMesh.FindNearestPoint(pathPos + Vector3(Random(-4.5f, 4.5f), 0.0f, Random(-4.5, 4.5f)), Vector3(1.0f, 1.0f, 1.0f));
-                    agent.SetMoveTarget(targetPos);
-                }
-            }
-        }
+            // Set crowd agents target position
+            cast<DetourCrowdManager>(scene_.GetComponent("DetourCrowdManager")).SetCrowdTarget(pathPos);
     }
 }
 
@@ -395,9 +365,9 @@ void MoveCamera(float timeStep)
 
     // Set destination or spawn a jack with left mouse button
     if (input.mouseButtonPress[MOUSEB_LEFT])
-        SetPathPoint();
+        SetPathPoint(input.qualifierDown[QUAL_SHIFT]);
     // Add new obstacle or remove existing obstacle/agent with middle mouse button
-    if (input.mouseButtonPress[MOUSEB_MIDDLE])
+    else if (input.mouseButtonPress[MOUSEB_MIDDLE])
         AddOrRemoveObject();
 
     // Check for loading/saving the scene from/to the file Data/Scenes/CrowdNavigation.xml relative to the executable directory
@@ -406,19 +376,22 @@ void MoveCamera(float timeStep)
         File saveFile(fileSystem.programDir + "Data/Scenes/CrowdNavigation.xml", FILE_WRITE);
         scene_.SaveXML(saveFile);
     }
-    if (input.keyPress[KEY_F7])
+    else if (input.keyPress[KEY_F7])
     {
         File loadFile(fileSystem.programDir + "Data/Scenes/CrowdNavigation.xml", FILE_READ);
         scene_.LoadXML(loadFile);
-
-        // After reload, reacquire navMesh & crowd manager
-        navMesh = scene_.GetComponent("DynamicNavigationMesh");
-        crowdManager = scene_.GetComponent("DetourCrowdManager");
     }
 
     // Toggle debug geometry with space
-    if (input.keyPress[KEY_SPACE])
+    else if (input.keyPress[KEY_SPACE])
         drawDebug = !drawDebug;
+
+    // Toggle instruction text with F12
+    else if (input.keyPress[KEY_F12])
+    {
+        UIElement@ instruction = ui.root.GetChild(INSTRUCTION);
+        instruction.visible = !instruction.visible;
+    }
 }
 
 void HandleUpdate(StringHash eventType, VariantMap& eventData)
@@ -428,25 +401,6 @@ void HandleUpdate(StringHash eventType, VariantMap& eventData)
 
     // Move the camera, scale movement with time step
     MoveCamera(timeStep);
-
-    // Make the Jack CrowdAgents face the direction of their velocity and update animation
-    Array<CrowdAgent@>@ agents = crowdManager.GetActiveAgents();
-    for (uint i = NUM_BARRELS; i < agents.length; ++i)
-    {
-        CrowdAgent@ agent = agents[i];
-        Node@ node = agent.node;
-        AnimationController@ animCtrl = node.GetComponent("AnimationController");
-        Vector3 velocity = agent.actualVelocity;
-
-        if (velocity.length < 0.6)
-            animCtrl.Stop("Models/Jack_Walk.ani", 0.2);
-        else
-        {
-            node.worldDirection = velocity;
-            animCtrl.PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.2);
-            animCtrl.SetSpeed("Models/Jack_Walk.ani", velocity.length * 0.3);
-        }
-    }
 }
 
 void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
@@ -454,9 +408,9 @@ void HandlePostRenderUpdate(StringHash eventType, VariantMap& eventData)
     if (drawDebug)
     {
         // Visualize navigation mesh, obstacles and off-mesh connections
-        navMesh.DrawDebugGeometry(true);
+        cast<DynamicNavigationMesh>(scene_.GetComponent("DynamicNavigationMesh")).DrawDebugGeometry(true);
         // Visualize agents' path and position to reach
-        crowdManager.DrawDebugGeometry(true);
+        cast<DetourCrowdManager>(scene_.GetComponent("DetourCrowdManager")).DrawDebugGeometry(true);
     }
 }
 
@@ -469,12 +423,39 @@ void HandleCrowdAgentFailure(StringHash eventType, VariantMap& eventData)
     if (state == CrowdAgentState::CROWD_AGENT_INVALID)
     {
         // Get a point on the navmesh using more generous extents
-        Vector3 newPos = navMesh.FindNearestPoint(node.position, Vector3(5.0f,5.0f,5.0f));
+        Vector3 newPos = cast<DynamicNavigationMesh>(scene_.GetComponent("DynamicNavigationMesh")).FindNearestPoint(node.position, Vector3(5.0f,5.0f,5.0f));
         // Set the new node position, CrowdAgent component will automatically reset the state of the agent
         node.position = newPos;
     }
 }
 
+void HandleCrowdAgentReposition(StringHash eventType, VariantMap& eventData)
+{
+    const String WALKING_ANI = "Models/Jack_Walk.ani";
+
+    Node@ node = eventData["Node"].GetPtr();
+    CrowdAgent@ agent = eventData["CrowdAgent"].GetPtr();
+    Vector3 velocity = eventData["Velocity"].GetVector3();
+
+    // Only Jack agent has animation controller
+    AnimationController@ animCtrl = node.GetComponent("AnimationController");
+    if (animCtrl !is null)
+    {
+        float speed = velocity.length;
+        if (speed < agent.radius)
+            // If speed is too low then stopping the animation
+            animCtrl.Stop(WALKING_ANI, 0.8f);
+        else
+        {
+            // Face the direction of its velocity
+            node.worldDirection = velocity;
+            animCtrl.Play(WALKING_ANI, 0, true);
+            // Throttle the animation speed based on agent speed ratio (ratio = 1 is full throttle)
+            animCtrl.SetSpeed(WALKING_ANI, speed / agent.maxSpeed);
+        }
+    }
+}
+
 // Create XML patch instructions for screen joystick layout specific to this sample app
 String patchInstructions =
         "<patch>" +