Browse Source

Added check to ListView to see if has been destroyed as a response to the selection event.
Added CharacterDemo C++ example to Extras.
Improved the QuickStart in C++ to be cross-platform.

Lasse Öörni 12 years ago
parent
commit
1d60bc59fa

+ 1 - 1
Bin/Data/Scripts/TestScene.as

@@ -210,7 +210,7 @@ void InitScene()
 
         RigidBody@ body = objectNode.CreateComponent("RigidBody");
         CollisionShape@ shape = objectNode.CreateComponent("CollisionShape");
-        shape.SetTriangleMesh(cache.GetResource("Model", "Models/Mushroom.mdl"), 0);
+        shape.SetTriangleMesh(object.model, 0);
     }
 
     for (uint i = 0; i < 50; ++i)

+ 4 - 4
Bin/Data/Scripts/TestSceneOld.as

@@ -117,15 +117,15 @@ void InitScene()
         newNode.position = Vector3(50, 0, 50);
         newNode.SetScale(10);
 
-        RigidBody@ body = newNode.CreateComponent("RigidBody");
-        CollisionShape@ shape = newNode.CreateComponent("CollisionShape");
-        shape.SetTriangleMesh(cache.GetResource("Model", "Models/Mushroom.mdl"), 0);
-
         StaticModel@ object = newNode.CreateComponent("StaticModel");
         object.model = cache.GetResource("Model", "Models/Mushroom.mdl");
         object.material = cache.GetResource("Material", "Materials/Mushroom.xml");
         object.castShadows = true;
         object.occluder = true;
+
+        RigidBody@ body = newNode.CreateComponent("RigidBody");
+        CollisionShape@ shape = newNode.CreateComponent("CollisionShape");
+        shape.SetTriangleMesh(object.model, 0);
     }
 
     // Create mushroom groups

+ 1 - 1
CMakeLists.txt

@@ -194,4 +194,4 @@ endif ()
 
 # Urho3D extras. Uncomment to enable
 # add_subdirectory (Extras/OgreBatchConverter)
-
+# add_subdirectory (Extras/CharacterDemo)

+ 14 - 9
Docs/GettingStarted.dox

@@ -442,8 +442,6 @@ The last command resets the launch service database and rebuilds it, so the chan
 
 This example shows how to create an Urho3D C++ application from the ground up. The actual functionality will be the same as in \ref ScriptQuickstart "Quickstart in script"; it is strongly recommended that you familiarize yourself with it first.
 
-For simplicity, the application is assumed to be compiled on Windows and therefore defines the WinMain() function; look at the file Urho3D.cpp in the Urho3D subdirectory on how to handle cross-platform startup using a macro defined in Main.h in the Core library.
-
 To start with, create a subdirectory "HelloWorld" into the Urho3D root directory, and add the following line to the end of the root directory's CMakeLists.txt %file:
 
 \code
@@ -467,6 +465,8 @@ set (LIBS ../Engine/Container ../Engine/Core ../Engine/Engine ../Engine/Graphics
 # Setup target
 if (WIN32)
     set (EXE_TYPE WIN32)
+elseif (APPLE)
+    set (CMAKE_EXE_LINKER_FLAGS "-framework AudioUnit -framework Carbon -framework Cocoa -framework CoreAudio -framework ForceFeedback -framework IOKit -framework OpenGL -framework CoreServices")
 endif ()
 setup_executable ()
 \endcode
@@ -494,7 +494,8 @@ To start with, we need the include files for all the engine classes we are going
 #include "Text.h"
 #include "UI.h"
 
-#include <Windows.h>
+#include "Main.h"
+#include "DebugNew.h"
 \endcode
 
 All Urho3D classes reside inside the namespace Urho3D, so a using directive is convenient:
@@ -503,7 +504,7 @@ All Urho3D classes reside inside the namespace Urho3D, so a using directive is c
 using namespace Urho3D;
 \endcode
 
-To be able to subscribe to events, we need to subclass Object (if we did not use events, we could do everything procedurally, for example directly in WinMain, but that would be somewhat ugly.) We name the class HelloWorld, with functions that match the script version, plus a constructor. Note the shared pointers to the scene that we will create, and to the ResourceCache, which is perhaps the most often used subsystem, and therefore convenient to store here. Also note the OBJECT(className) macro, which inserts code for object type identification:
+To be able to subscribe to events, we need to subclass Object (if we did not use events, we could do everything procedurally, for example directly in the main function we define, but that would be somewhat ugly.) We name the class HelloWorld, with functions that match the script version, plus a constructor. Note the shared pointers to the scene that we will create, and to the ResourceCache, which is perhaps the most often used subsystem, and therefore convenient to store here. Also note the OBJECT(className) macro, which inserts code for object type identification:
 
 \code
 class HelloWorld : public Object
@@ -523,25 +524,27 @@ public:
 };
 \endcode
 
-Before the actual HelloWorld implementation, we define WinMain.
+Before the actual HelloWorld implementation, we define the program entry point. The earlier included Main.h contains a DEFINE_MAIN macro which helps to create a cross-platform entry point (on Windows it will be WinMain(), on Linux just main() etc.) It parses the command line and then returns control to the user-defined function.
 
 \code
-int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE prevInstance, PSTR cmdLine, int showCmd)
+int Run()
 {
     SharedPtr<Context> context(new Context());
     SharedPtr<Engine> engine(new Engine(context));
-    engine->Initialize("HelloWorld", "HelloWorld.log", ParseArguments(GetCommandLineW()));
+    engine->Initialize("HelloWorld", "HelloWorld.log", GetArguments());
 
     SharedPtr<HelloWorld> helloWorld(new HelloWorld(context));
     helloWorld->Start();
     while (!engine->IsExiting())
         engine->RunFrame();
-
+        
     return 0;
 }
+
+DEFINE_MAIN(Run())
 \endcode
 
-First, we create the Context object, which holds all subsystems and object factories, and keeps track of event senders and receivers. All Object subclasses need to be supplied a pointer to that context. When using an object factory (such as when creating components) that is automatic, but when creating objects manually, the pointer also needs to be passed manually.
+In our function, we first create the Context object, which holds all subsystems and object factories, and keeps track of event senders and receivers. All Object subclasses need to be supplied a pointer to that context. When using an object factory (such as when creating components) that is automatic, but when creating objects manually, the pointer also needs to be passed manually.
 
 With the context at hand, we create the Engine and initialize it. The arguments for the \ref Engine::Initialize "Initialize()" function are the initial window title, the log file name, and command line parameters, which are parsed using the ParseArguments() helper function.
 
@@ -645,6 +648,8 @@ void HelloWorld::HandleUpdate(StringHash eventType, VariantMap& eventData)
 
 Now you should be ready to compile HelloWorld.cpp. The resulting executable will be placed in the Bin directory. It should be substantially smaller than Urho3D.exe due to leaving out the scripting functionality.
 
+For a more complex C++ example, check out CharacterDemo in the Extras directory, which creates the same static scene as the TestScene script example, and demonstrates a 1st/3rd person controllable character. To enable it in the build, uncomment from the bottom of the root CMakeLists.txt.
+
 
 \page EditorInstructions Editor instructions
 

+ 29 - 14
Engine/UI/ListView.cpp

@@ -469,8 +469,11 @@ void ListView::SetSelection(unsigned index)
 
 void ListView::SetSelections(const PODVector<unsigned>& indices)
 {
+    // Make a weak pointer to self to check for destruction as a response to events
+    WeakPtr<ListView> self(this);
+    
     unsigned numItems = GetNumItems();
-
+    
     // Remove first items that should no longer be selected
     for (PODVector<unsigned>::Iterator i = selections_.Begin(); i != selections_.End();)
     {
@@ -478,20 +481,23 @@ void ListView::SetSelections(const PODVector<unsigned>& indices)
         if (!indices.Contains(index))
         {
             i = selections_.Erase(i);
-
+            
             using namespace ItemSelected;
-
+            
             VariantMap eventData;
             eventData[P_ELEMENT] = (void*)this;
             eventData[P_SELECTION] = index;
             SendEvent(E_ITEMDESELECTED, eventData);
+            
+            if (self.Expired())
+                return;
         }
         else
             ++i;
     }
-
+    
     bool added = false;
-
+    
     // Then add missing items
     for (PODVector<unsigned>::ConstIterator i = indices.Begin(); i != indices.End(); ++i)
     {
@@ -507,51 +513,60 @@ void ListView::SetSelections(const PODVector<unsigned>& indices)
                     selections_.Push(index);
                     added = true;
                 }
-
+                
                 using namespace ItemSelected;
-
+                
                 VariantMap eventData;
                 eventData[P_ELEMENT] = (void*)this;
                 eventData[P_SELECTION] = *i;
                 SendEvent(E_ITEMSELECTED, eventData);
+                
+                if (self.Expired())
+                    return;
             }
         }
         // If no multiselect enabled, allow setting only one item
         if (!multiselect_)
             break;
     }
-
+    
     // Re-sort selections if necessary
     if (added)
         Sort(selections_.Begin(), selections_.End());
-
+    
     UpdateSelectionEffect();
     SendEvent(E_SELECTIONCHANGED);
 }
 
 void ListView::AddSelection(unsigned index)
 {
+    // Make a weak pointer to self to check for destruction as a response to events
+    WeakPtr<ListView> self(this);
+    
     if (!multiselect_)
         SetSelection(index);
     else
     {
         if (index >= GetNumItems())
             return;
-
+        
         if (!selections_.Contains(index))
         {
             selections_.Push(index);
-
+            
             using namespace ItemSelected;
-
+            
             VariantMap eventData;
             eventData[P_ELEMENT] = (void*)this;
             eventData[P_SELECTION] = index;
             SendEvent(E_ITEMSELECTED, eventData);
-
+            
+            if (self.Expired())
+                return;
+            
             Sort(selections_.Begin(), selections_.End());
         }
-
+        
         EnsureItemVisibility(index);
         UpdateSelectionEffect();
         SendEvent(E_SELECTIONCHANGED);

+ 19 - 0
Extras/CharacterDemo/CMakeLists.txt

@@ -0,0 +1,19 @@
+# Define target name
+set (TARGET_NAME CharacterDemo)
+
+# Define source files
+file (GLOB CPP_FILES *.cpp)
+file (GLOB H_FILES *.h)
+set (SOURCE_FILES ${CPP_FILES} ${H_FILES})
+
+# Define dependency libs
+set (LIBS ../../Engine/Container ../../Engine/Core ../../Engine/Engine ../../Engine/Graphics ../../Engine/Input ../../Engine/IO ../../Engine/Math 
+    ../../Engine/Network ../../Engine/Physics ../../Engine/Resource ../../Engine/Scene ../../Engine/UI ../../ThirdParty/Bullet/src)
+
+# Setup target
+if (WIN32)
+    set (EXE_TYPE WIN32)
+elseif (APPLE)
+    set (CMAKE_EXE_LINKER_FLAGS "-framework AudioUnit -framework Carbon -framework Cocoa -framework CoreAudio -framework ForceFeedback -framework IOKit -framework OpenGL -framework CoreServices")
+endif ()
+setup_executable ()

+ 464 - 0
Extras/CharacterDemo/CharacterDemo.cpp

@@ -0,0 +1,464 @@
+#include "AnimatedModel.h"
+#include "AnimationController.h"
+#include "Camera.h"
+#include "CollisionShape.h"
+#include "Context.h"
+#include "Controls.h"
+#include "CoreEvents.h"
+#include "DebugRenderer.h"
+#include "Engine.h"
+#include "Font.h"
+#include "Input.h"
+#include "Light.h"
+#include "Material.h"
+#include "MemoryBuffer.h"
+#include "Model.h"
+#include "Octree.h"
+#include "PhysicsEvents.h"
+#include "PhysicsWorld.h"
+#include "ProcessUtils.h"
+#include "Renderer.h"
+#include "RigidBody.h"
+#include "ResourceCache.h"
+#include "Scene.h"
+#include "StaticModel.h"
+#include "Text.h"
+#include "UI.h"
+#include "Zone.h"
+
+#include "Main.h"
+#include "DebugNew.h"
+
+using namespace Urho3D;
+
+const int CTRL_UP = 1;
+const int CTRL_DOWN = 2;
+const int CTRL_LEFT = 4;
+const int CTRL_RIGHT = 8;
+const int CTRL_FIRE = 16;
+const int CTRL_JUMP = 32;
+const int CTRL_ALL = 63;
+
+const float MOVE_FORCE = 0.8f;
+const float INAIR_MOVE_FORCE = 0.02f;
+const float BRAKE_FORCE = 0.2f;
+const float JUMP_FORCE = 7.0f;
+const float YAW_SENSITIVITY = 0.1f;
+const float INAIR_THRESHOLD_TIME = 0.1f;
+
+const float CAMERA_MIN_DIST = 1.0f;
+const float CAMERA_MAX_DIST = 5.0f;
+const float CAMERA_SAFETY_DIST = 0.5f;
+
+class Character : public Component
+{
+    OBJECT(Character)
+
+public:
+    /// Construct.
+    Character(Context* context);
+    
+    /// Handle node being assigned.
+    virtual void OnNodeSet(Node* node);
+    
+    /// Handle physics collision event.
+    void HandleNodeCollision(StringHash eventType, VariantMap& eventData);
+    /// Handle physics world update event.
+    void HandleFixedUpdate(StringHash eventType, VariantMap& eventData);
+    
+    /// Movement controls.
+    Controls controls_;
+    /// Grounded flag for movement.
+    bool onGround_;
+    /// Jump flag.
+    bool okToJump_;
+    /// In air timer. Due to possible physics inaccuracy, character can be off ground for max. 1/10 second and still be allowed to move.
+    float inAirTimer_;
+};
+
+class CharacterDemo : public Object
+{
+    OBJECT(CharacterDemo);
+
+public:
+    /// Construct.
+    CharacterDemo(Context* context);
+    
+    /// Initialize.
+    void Start();
+    
+private:
+    /// Create static scene content.
+    void CreateScene();
+    /// Create controllable character.
+    void CreateCharacter();
+    /// Subscribe to necessary events.
+    void SubscribeToEvents();
+    /// Handle application update. Set controls to character & check global keys.
+    void HandleUpdate(StringHash eventType, VariantMap& eventData);
+    /// Handle application post-update. Update camera position after character has moved.
+    void HandlePostUpdate(StringHash eventType, VariantMap& eventData);
+    
+    /// Scene.
+    SharedPtr<Scene> scene_;
+    /// Resource cache subsystem, stored here for convenience.
+    SharedPtr<ResourceCache> cache_;
+    /// Camera scene node.
+    SharedPtr<Node> cameraNode_;
+    /// The controllable character.
+    SharedPtr<Character> character_;
+    /// First person camera flag.
+    bool firstPerson_;
+};
+
+int Run()
+{
+    SharedPtr<Context> context(new Context());
+    SharedPtr<Engine> engine(new Engine(context));
+    engine->Initialize("Character", "Character.log", GetArguments());
+    
+    SharedPtr<CharacterDemo> characterDemo(new CharacterDemo(context));
+    characterDemo->Start();
+    while (!engine->IsExiting())
+        engine->RunFrame();
+    
+    return 0;
+}
+
+DEFINE_MAIN(Run())
+
+OBJECTTYPESTATIC(CharacterDemo);
+OBJECTTYPESTATIC(Character);
+
+CharacterDemo::CharacterDemo(Context* context) :
+    Object(context),
+    cache_(GetSubsystem<ResourceCache>()),
+    firstPerson_(false)
+{
+    // Register factory for the Character component so it can be created via CreateComponent
+    context_->RegisterFactory<Character>();
+}
+
+void CharacterDemo::Start()
+{
+    CreateScene();
+    CreateCharacter();
+    SubscribeToEvents();
+}
+
+void CharacterDemo::CreateScene()
+{
+    scene_ = new Scene(context_);
+    
+    // Create scene subsystem components
+    scene_->CreateComponent<Octree>();
+    scene_->CreateComponent<PhysicsWorld>();
+    scene_->CreateComponent<DebugRenderer>();
+    
+    // Create camera and define viewport. Camera does not necessarily have to belong to the scene
+    cameraNode_ = new Node(context_);
+    Camera* camera = cameraNode_->CreateComponent<Camera>();
+    GetSubsystem<Renderer>()->SetViewport(0, new Viewport(context_, scene_, camera));
+    
+    // Create static scene content, same as in TestScene.as
+    Node* zoneNode = scene_->CreateChild("Zone");
+    Zone* zone = zoneNode->CreateComponent<Zone>();
+    zone->SetAmbientColor(Color(0.15f, 0.15f, 0.15f));
+    zone->SetFogColor(Color(0.5f, 0.5f, 0.7f));
+    zone->SetFogStart(100.0f);
+    zone->SetFogEnd(300.0f);
+    zone->SetBoundingBox(BoundingBox(-1000.0f, 1000.0f));
+    
+    {
+        Node* lightNode = scene_->CreateChild("GlobalLight");
+        lightNode->SetDirection(Vector3(0.3f, -0.5f, 0.425f));
+        
+        Light* light = lightNode->CreateComponent<Light>();
+        light->SetLightType(LIGHT_DIRECTIONAL);
+        light->SetCastShadows(true);
+        light->SetShadowBias(BiasParameters(0.0001f, 0.5f));
+        light->SetShadowCascade(CascadeParameters(10.0f, 50.0f, 200.0f, 0.0f, 0.8f));
+        light->SetSpecularIntensity(0.5f);
+    }
+
+    {
+        Node* objectNode = scene_->CreateChild("Floor");
+        objectNode->SetPosition(Vector3(0.0f, -0.5f, 0.0f));
+        objectNode->SetScale(Vector3(200.0f, 1.0f, 200.0f));
+        
+        StaticModel* object = objectNode->CreateComponent<StaticModel>();
+        object->SetModel(cache_->GetResource<Model>("Models/Box.mdl"));
+        object->SetMaterial(cache_->GetResource<Material>("Materials/Stone.xml"));
+        object->SetOccluder(true);
+        
+        RigidBody* body = objectNode->CreateComponent<RigidBody>();
+        CollisionShape* shape = objectNode->CreateComponent<CollisionShape>();
+        shape->SetBox(Vector3::ONE);
+    }
+    
+    for (unsigned i = 0; i < 50; ++i)
+    {
+        Node* objectNode = scene_->CreateChild("Box");
+        objectNode->SetPosition(Vector3(Random() * 180.0f - 90.0f, 1.0f, Random() * 180.0f - 90.0f));
+        objectNode->SetScale(2.0f);
+        
+        StaticModel* object = objectNode->CreateComponent<StaticModel>();
+        object->SetModel(cache_->GetResource<Model>("Models/Box.mdl"));
+        object->SetMaterial(cache_->GetResource<Material>("Materials/Stone.xml"));
+        object->SetCastShadows(true);
+        
+        RigidBody* body = objectNode->CreateComponent<RigidBody>();
+        CollisionShape* shape = objectNode->CreateComponent<CollisionShape>();
+        shape->SetBox(Vector3::ONE);
+    }
+    
+    for (unsigned i = 0; i < 10; ++i)
+    {
+        Node* objectNode = scene_->CreateChild("Box");
+        objectNode->SetPosition(Vector3(Random() * 180.0f - 90.0f, 10.0f, Random() * 180.0f - 90.0f));
+        objectNode->SetScale(20);
+        
+        StaticModel* object = objectNode->CreateComponent<StaticModel>();
+        object->SetModel(cache_->GetResource<Model>("Models/Box.mdl"));
+        object->SetMaterial(cache_->GetResource<Material>("Materials/Stone.xml"));
+        object->SetCastShadows(true);
+        object->SetOccluder(true);
+        
+        RigidBody* body = objectNode->CreateComponent<RigidBody>();
+        CollisionShape* shape = objectNode->CreateComponent<CollisionShape>();
+        shape->SetBox(Vector3::ONE);
+    }
+
+    for (unsigned i = 0; i < 50; ++i)
+    {
+        Node* objectNode = scene_->CreateChild("Mushroom");
+        objectNode->SetPosition(Vector3(Random() * 180.0f - 90.0f, 0.0f, Random() * 180.0f - 90.0f));
+        objectNode->SetRotation(Quaternion(0.0f, Random(360.0f), 0.0f));
+        objectNode->SetScale(5.0f);
+        
+        StaticModel* object = objectNode->CreateComponent<StaticModel>();
+        object->SetModel(cache_->GetResource<Model>("Models/Mushroom.mdl"));
+        object->SetMaterial(cache_->GetResource<Material>("Materials/Mushroom.xml"));
+        object->SetCastShadows(true);
+        
+        RigidBody* body = objectNode->CreateComponent<RigidBody>();
+        CollisionShape* shape = objectNode->CreateComponent<CollisionShape>();
+        shape->SetTriangleMesh(object->GetModel(), 0);
+    }
+}
+
+void CharacterDemo::CreateCharacter()
+{
+    Node* objectNode = scene_->CreateChild("Jack");
+    objectNode->SetPosition(Vector3(0.0f, 1.0f, 0.0f));
+    
+    AnimatedModel* object = objectNode->CreateComponent<AnimatedModel>();
+    object->SetModel(cache_->GetResource<Model>("Models/Jack.mdl"));
+    object->SetMaterial(cache_->GetResource<Material>("Materials/Jack.xml"));
+    object->SetCastShadows(true);
+    
+    AnimationController* ctrl = objectNode->CreateComponent<AnimationController>();
+    
+    // Create rigidbody, and set non-zero mass so that the body becomes dynamic
+    RigidBody* body = objectNode->CreateComponent<RigidBody>();
+    body->SetMass(1.0f);
+    
+    // Set zero angular factor so that physics doesn't turn the character on its own.
+    // Instead we will control the character yaw manually
+    body->SetAngularFactor(Vector3::ZERO);
+    
+    // Set the rigid body to signal collision also when in rest, so that we get ground collisions properly
+    body->SetCollisionEventMode(COLLISION_ALWAYS);
+    
+    // Set a capsule shape for character collision
+    CollisionShape* shape = objectNode->CreateComponent<CollisionShape>();
+    shape->SetCapsule(0.7f, 1.8f, Vector3(0.0f, 0.9f, 0.0f));
+    
+    // Create our logic component, which takes care of steering the character with key/mouse input
+    // Remember the character so that we can set controls to it
+    character_ = objectNode->CreateComponent<Character>();
+}
+
+void CharacterDemo::SubscribeToEvents()
+{
+    SubscribeToEvent(E_UPDATE, HANDLER(CharacterDemo, HandleUpdate));
+    SubscribeToEvent(E_POSTUPDATE, HANDLER(CharacterDemo, HandlePostUpdate));
+}
+
+void CharacterDemo::HandleUpdate(StringHash eventType, VariantMap& eventData)
+{
+    float timeStep = eventData[Update::P_TIMESTEP].GetFloat();
+    Input* input = GetSubsystem<Input>();
+
+    if (input->GetKeyDown(KEY_ESC))
+        GetSubsystem<Engine>()->Exit();
+    
+    if (input->GetKeyPress('F'))
+        firstPerson_ = !firstPerson_;
+    
+    // Get movement controls and assign them to the character
+    character_->controls_.Set(CTRL_UP, input->GetKeyDown('W'));
+    character_->controls_.Set(CTRL_DOWN, input->GetKeyDown('S'));
+    character_->controls_.Set(CTRL_LEFT, input->GetKeyDown('A'));
+    character_->controls_.Set(CTRL_RIGHT, input->GetKeyDown('D'));
+    character_->controls_.Set(CTRL_JUMP, input->GetKeyDown(KEY_SPACE));
+    
+    // Add character yaw & pitch from the mouse motion
+    character_->controls_.yaw_ += (float)input->GetMouseMoveX() * YAW_SENSITIVITY;
+    character_->controls_.pitch_ += (float)input->GetMouseMoveY() * YAW_SENSITIVITY;
+    // Limit pitch
+    character_->controls_.pitch_ = Clamp(character_->controls_.pitch_, -80.0f, 80.0f);
+    
+    // Set rotation already here so that it's updated every rendering frame instead of every physics frame
+    character_->GetNode()->SetRotation(Quaternion(character_->controls_.yaw_, Vector3::UP));
+}
+
+void CharacterDemo::HandlePostUpdate(StringHash eventType, VariantMap& eventData)
+{
+    Node* characterNode = character_->GetNode();
+    
+    // Get camera lookat dir from character yaw + pitch
+    Quaternion rot = characterNode->GetRotation();
+    Quaternion dir = rot * Quaternion(character_->controls_.pitch_, Vector3::RIGHT);
+    
+    if (firstPerson_)
+    {
+        // First person camera: position to the head bone + offset slightly forward & up
+        Node* headNode = characterNode->GetChild("Bip01_Head", true);
+        if (headNode)
+        {
+            cameraNode_->SetPosition(headNode->GetWorldPosition() + dir * Vector3(0.0f, 0.2f, 0.2f));
+            cameraNode_->SetRotation(dir);
+        }
+    }
+    else
+    {
+        // Third person camera: position behind the character
+        Vector3 aimPoint = characterNode->GetPosition() + Vector3(0.0f, 1.75f, 0.0f);
+        Vector3 minDist = aimPoint + dir * Vector3(0, 0, -CAMERA_MIN_DIST);
+        Vector3 maxDist = aimPoint + dir * Vector3(0, 0, -CAMERA_MAX_DIST);
+        
+        // Collide camera ray with physics world to ensure we see the character properly
+        Vector3 rayDir = (maxDist - minDist).Normalized();
+        float rayDistance = CAMERA_MAX_DIST - CAMERA_MIN_DIST + CAMERA_SAFETY_DIST;
+        PhysicsRaycastResult result;
+        scene_->GetComponent<PhysicsWorld>()->RaycastSingle(result, Ray(minDist, rayDir), rayDistance);
+        if (result.body_)
+            rayDistance = Min(rayDistance, result.distance_ - CAMERA_SAFETY_DIST);
+        
+        cameraNode_->SetPosition(minDist + rayDir * rayDistance);
+        cameraNode_->SetRotation(dir);
+    }
+}
+
+Character::Character(Context* context) :
+    Component(context),
+    onGround_(false),
+    okToJump_(true),
+    inAirTimer_(0.0f)
+{
+}
+
+void Character::OnNodeSet(Node* node)
+{
+    // Component has been inserted into its scene node. Subscribe to events now
+    SubscribeToEvent(node, E_NODECOLLISION, HANDLER(Character, HandleNodeCollision));
+    SubscribeToEvent(GetScene()->GetComponent<PhysicsWorld>(), E_PHYSICSPRESTEP, HANDLER(Character, HandleFixedUpdate));
+}
+
+void Character::HandleNodeCollision(StringHash eventType, VariantMap& eventData)
+{
+    // Check collision contacts and see if character is standing on ground (look for a contact that has near vertical normal)
+    using namespace NodeCollision;
+    
+    MemoryBuffer contacts(eventData[P_CONTACTS].GetBuffer());
+    
+    while (!contacts.IsEof())
+    {
+        Vector3 contactPosition = contacts.ReadVector3();
+        Vector3 contactNormal = contacts.ReadVector3();
+        float contactDistance = contacts.ReadFloat();
+        float contactImpulse = contacts.ReadFloat();
+        
+        // If contact is below node center and mostly vertical, assume it's a ground contact
+        if (contactPosition.y_ < (node_->GetPosition().y_ + 1.0f))
+        {
+            float level = Abs(contactNormal.y_);
+            if (level > 0.75)
+                onGround_ = true;
+        }
+    }
+}
+
+void Character::HandleFixedUpdate(StringHash eventType, VariantMap& eventData)
+{
+    using namespace PhysicsPreStep;
+    
+    float timeStep = eventData[P_TIMESTEP].GetFloat();
+    
+    /// \todo Could cache the components for faster access instead of finding them each frame
+    RigidBody* body = GetComponent<RigidBody>();
+    AnimationController* animCtrl = GetComponent<AnimationController>();
+    
+    // Update the in air timer. Reset if grounded
+    if (!onGround_)
+        inAirTimer_ += timeStep;
+    else
+        inAirTimer_ = 0.0f;
+    // When character has been in air less than 1/10 second, still interpreted as being on ground
+    bool softGrounded = inAirTimer_ < INAIR_THRESHOLD_TIME;
+    
+    // Update movement & animation
+    const Quaternion& rot = node_->GetRotation();
+    Vector3 moveDir = Vector3::ZERO;
+    const Vector3& velocity = body->GetLinearVelocity();
+    // Velocity on the XZ plane
+    Vector3 planeVelocity(velocity.x_, 0.0f, velocity.z_);
+    
+    if (controls_.IsDown(CTRL_UP))
+        moveDir += Vector3::FORWARD;
+    if (controls_.IsDown(CTRL_DOWN))
+        moveDir += Vector3::BACK;
+    if (controls_.IsDown(CTRL_LEFT))
+        moveDir += Vector3::LEFT;
+    if (controls_.IsDown(CTRL_RIGHT))
+        moveDir += Vector3::RIGHT;
+    
+    // Normalize move vector so that diagonal strafing is not faster
+    if (moveDir.LengthSquared() > 0.0f)
+        moveDir.Normalize();
+    
+    // If in air, allow control, but slower than when on ground
+    body->ApplyImpulse(rot * moveDir * (softGrounded ? MOVE_FORCE : INAIR_MOVE_FORCE));
+    
+    if (softGrounded)
+    {
+        // When on ground, apply a braking force to limit maximum ground velocity
+        Vector3 brakeForce = -planeVelocity * BRAKE_FORCE;
+        body->ApplyImpulse(brakeForce);
+        
+        // Jump. Must release jump control inbetween jumps
+        if (controls_.IsDown(CTRL_JUMP))
+        {
+            if (okToJump_)
+            {
+                body->ApplyImpulse(Vector3::UP * JUMP_FORCE);
+                okToJump_ = false;
+            }
+        }
+        else
+            okToJump_ = true;
+    }
+    
+    // Play or stop (fade out) walk animation based on whether is moving
+    if (softGrounded && !moveDir.Equals(Vector3::ZERO))
+        animCtrl->PlayExclusive("Models/Jack_Walk.ani", 0, true, 0.2f);
+    else
+        animCtrl->Stop("Models/Jack_Walk.ani", 0.2f);
+    // Set walk animation speed proportional to velocity
+    animCtrl->SetSpeed("Models/Jack_Walk.ani", planeVelocity.Length() * 0.3f);
+    
+    // Reset grounded flag for next frame
+    onGround_ = false;
+}
+

+ 6 - 0
Extras/Readme.txt

@@ -10,3 +10,9 @@ OgreMaxscriptExport
   Exporter from the Ogre SDK that will import Ogre .mesh.xml files (for feeding
   into OgreImporter) and materials in Urho3D .xml format.
 
+CharacterDemo
+
+- A moving character example, with 1st/3rd person camera. Creates the same scene as 
+  the TestScene script application.
+- Enable in build by uncommenting at the bottom of the Urho3D root CMakeLists.txt
+