Browse Source

Added animation trigger event system.
Added ReplaceExtension() utility function to simplify filename manipulation code.
Cleaned up NinjaSnowWar script code. Added particle effect on ninja's footsteps and landing from jump.

Lasse Öörni 13 năm trước cách đây
mục cha
commit
3b67320a49

+ 9 - 0
Bin/Data/Objects/Ninja.xml

@@ -108,6 +108,15 @@
 		<component type="AnimationController" id="7">
 		<component type="AnimationController" id="7">
 			<attribute name="Animations" />
 			<attribute name="Animations" />
 		</component>
 		</component>
+    	<component type="ScriptInstance" id="8">
+    		<attribute name="Script File" value="ScriptFile;Scripts/NinjaSnowWar.as" />
+    		<attribute name="Class Name" value="FootSteps" />
+    		<attribute name="Is Active" value="true" />
+    		<attribute name="Fixed Update FPS" value="0" />
+    		<attribute name="Time Accumulator" value="0" />
+    		<attribute name="Delayed Method Calls" value="0" />
+    		<attribute name="Script Data" value="" />
+    	</component>
 		<node id="16777216">
 		<node id="16777216">
 			<attribute name="Name" value="Joint1" />
 			<attribute name="Name" value="Joint1" />
 			<attribute name="Position" value="0.185046 0.51196 -0" />
 			<attribute name="Position" value="0.185046 0.51196 -0" />

+ 16 - 0
Bin/Data/Particle/SnowExplosionFade.xml

@@ -0,0 +1,16 @@
+<particleemitter>
+    <material name="Materials/Particle.xml" />
+    <updateinvisible enable="true" />
+    <numparticles value="10" />
+    <activetime value="0.1" />
+    <inactivetime value="0" />
+    <interval value="0.02" />
+    <sorting enable="true" />
+    <direction min="-1 0 -1" max="1 1 1" />
+    <velocity min="50" max="100" />
+    <particlesize value="25 25" />
+    <timetolive value="0.5" />
+    <constantforce value="0.0 -200 0.0" />
+    <colorfade color="0.125 0.125 0.25 1.0" time="0.0" />
+    <colorfade color="0.0 0.0 0.0 1.0" time="0.5" />
+</particleemitter>

+ 36 - 1
Bin/Data/Scripts/NinjaSnowWar.as

@@ -1,5 +1,6 @@
 // Remake of NinjaSnowWar in script
 // Remake of NinjaSnowWar in script
 
 
+#include "Scripts/NinjaSnowWar/FootSteps.as"
 #include "Scripts/NinjaSnowWar/LightFlash.as"
 #include "Scripts/NinjaSnowWar/LightFlash.as"
 #include "Scripts/NinjaSnowWar/Ninja.as"
 #include "Scripts/NinjaSnowWar/Ninja.as"
 #include "Scripts/NinjaSnowWar/Player.as"
 #include "Scripts/NinjaSnowWar/Player.as"
@@ -705,7 +706,41 @@ void SendHiscores(int playerIndex)
 Node@ SpawnObject(const Vector3&in position, const Quaternion&in rotation, const String&in className)
 Node@ SpawnObject(const Vector3&in position, const Quaternion&in rotation, const String&in className)
 {
 {
     XMLFile@ xml = cache.GetResource("XMLFile", "Objects/" + className + ".xml");
     XMLFile@ xml = cache.GetResource("XMLFile", "Objects/" + className + ".xml");
-    return gameScene.InstantiateXML(xml, position, rotation);
+    return scene.InstantiateXML(xml, position, rotation);
+}
+
+Node@ SpawnParticleEffect(const Vector3&in position, const String&in effectName, float duration, CreateMode mode = REPLICATED)
+{
+    Node@ newNode = scene.CreateChild("", mode);
+    newNode.position = position;
+
+    // Create the particle emitter
+    ParticleEmitter@ emitter = newNode.CreateComponent("ParticleEmitter");
+    emitter.parameters = cache.GetResource("XMLFile", effectName);
+
+    // Create a GameObject for managing the effect lifetime
+    GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL));
+    object.duration = duration;
+
+    return newNode;
+}
+
+Node@ SpawnSound(const Vector3&in position, const String&in soundName, float duration)
+{
+    Node@ newNode = scene.CreateChild();
+    newNode.position = position;
+
+    // Create the sound source
+    SoundSource3D@ source = newNode.CreateComponent("SoundSource3D");
+    Sound@ sound = cache.GetResource("Sound", soundName);
+    source.SetDistanceAttenuation(200, 5000, 1);
+    source.Play(sound);
+
+    // Create a GameObject for managing the sound lifetime
+    GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL));
+    object.duration = duration;
+
+    return newNode;
 }
 }
 
 
 void SpawnObjects(float timeStep)
 void SpawnObjects(float timeStep)

+ 25 - 0
Bin/Data/Scripts/NinjaSnowWar/FootSteps.as

@@ -0,0 +1,25 @@
+class FootSteps : ScriptObject
+{
+    void Start()
+    {
+        // Subscribe to animation triggers, which are sent by the AnimatedModel's node (same as our node)
+        SubscribeToEvent(node, "AnimationTrigger", "HandleAnimationTrigger");
+    }
+
+    void HandleAnimationTrigger(StringHash eventType, VariantMap& eventData)
+    {
+        AnimatedModel@ model = node.GetComponent("AnimatedModel");
+        AnimationState@ state = model.animationStates[eventData["Name"].GetString()];
+        if (state is null)
+            return;
+
+        // If the animation is blended with sufficient weight, instantiate a local particle effect for the footstep.
+        // The trigger data (string) tells the bone scenenode to use. Note: called on both client and server
+        if (state.weight > 0.5f)
+        {
+            Node@ bone = node.GetChild(eventData["Data"].GetString(), true);
+            if (bone !is null)
+                SpawnParticleEffect(bone.worldPosition, "Particle/SnowExplosionFade.xml", 1, LOCAL);
+        }
+    }
+}

+ 2 - 42
Bin/Data/Scripts/NinjaSnowWar/GameObject.as

@@ -34,8 +34,8 @@ class GameObject : ScriptObject
         lastDamageCreatorID = 0;
         lastDamageCreatorID = 0;
         creatorID = 0;
         creatorID = 0;
 
 
-        if (runClient)
-            Print("Warning! Logic object created on client!");
+        // if (runClient)
+        //     Print("Warning! Logic object created on client!");
     }
     }
 
 
     void FixedUpdate(float timeStep)
     void FixedUpdate(float timeStep)
@@ -79,46 +79,6 @@ class GameObject : ScriptObject
         source.autoRemove = true;
         source.autoRemove = true;
     }
     }
 
 
-    Node@ SpawnObject(const Vector3&in position, const Quaternion&in rotation, const String&in className)
-    {
-        XMLFile@ xml = cache.GetResource("XMLFile", "Objects/" + className + ".xml");
-        return scene.InstantiateXML(xml, position, rotation);
-    }
-
-    Node@ SpawnParticleEffect(const Vector3&in position, const String&in effectName, float duration)
-    {
-        Node@ newNode = scene.CreateChild();
-        newNode.position = position;
-
-        // Create the particle emitter
-        ParticleEmitter@ emitter = newNode.CreateComponent("ParticleEmitter");
-        emitter.parameters = cache.GetResource("XMLFile", effectName);
-
-        // Create a GameObject for managing the effect lifetime
-        GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL));
-        object.duration = duration;
-
-        return newNode;
-    }
-
-    Node@ SpawnSound(const Vector3&in position, const String&in soundName, float duration)
-    {
-        Node@ newNode = scene.CreateChild();
-        newNode.position = position;
-
-        // Create the sound source
-        SoundSource3D@ source = newNode.CreateComponent("SoundSource3D");
-        Sound@ sound = cache.GetResource("Sound", soundName);
-        source.SetDistanceAttenuation(200, 5000, 1);
-        source.Play(sound);
-
-        // Create a GameObject for managing the sound lifetime
-        GameObject@ object = cast<GameObject>(newNode.CreateScriptObject(scriptFile, "GameObject", LOCAL));
-        object.duration = duration;
-
-        return newNode;
-    }
-
     void HandleNodeCollision(StringHash eventType, VariantMap& eventData)
     void HandleNodeCollision(StringHash eventType, VariantMap& eventData)
     {
     {
         Node@ otherNode = eventData["OtherNode"].GetNode();
         Node@ otherNode = eventData["OtherNode"].GetNode();

+ 4 - 0
Bin/Data/Scripts/NinjaSnowWar/Ninja.as

@@ -99,6 +99,10 @@ class Ninja : GameObject
         Vector3 vel = body.linearVelocity;
         Vector3 vel = body.linearVelocity;
         if (onGround)
         if (onGround)
         {
         {
+            // If landed, play a particle effect at feet (use the AnimatedModel node)
+            if (inAirTime > 0.5)
+                SpawnParticleEffect(node.children[0].worldPosition, "Particle/SnowExplosion.xml", 1);
+
             inAirTime = 0;
             inAirTime = 0;
             onGroundTime += timeStep;
             onGroundTime += timeStep;
         }
         }

+ 14 - 1
Docs/Reference.dox

@@ -703,6 +703,19 @@ Note that AnimationController does not by default stop non-looping animations au
 
 
 By default an Animation is played back by using all the available bone tracks. However an animation can be only partially applied by setting a start bone, see \ref AnimationState::SetStartBone "SetStartBone()". Once set, the bone tracks will be applied hierarchically starting from the start bone. For example, to apply an animation only to a bipedal character's upper body, which is typically parented to the spine bone, one could set the spine as the start bone.
 By default an Animation is played back by using all the available bone tracks. However an animation can be only partially applied by setting a start bone, see \ref AnimationState::SetStartBone "SetStartBone()". Once set, the bone tracks will be applied hierarchically starting from the start bone. For example, to apply an animation only to a bipedal character's upper body, which is typically parented to the spine bone, one could set the spine as the start bone.
 
 
+\section SkeletalAnimation_Triggers Animation triggers
+
+Animations can be accompanied with trigger data that contains timestamped Variant data to be interpreted by the application. This trigger data is in XML format next to the animation file itself. When an animation contains triggers, the AnimatedModel's scene node sends the E_ANIMATIONTRIGGER event each time a trigger point is crossed. The event data contains the timestamp, the animation name, and the variant data. Triggers will fire when the animation is advanced using \ref AnimationState::AddTime "AddTime()", but not when setting the absolute animation time position.
+
+The trigger data definition is below. Either normalized (0 = animation start, 1 = animation end) or non-normalized (time in seconds) timestamps can be used. See Bin/Data/Models/Ninja_Walk.xml and Bin/Data/Models/Ninja_Stealth.xml for examples; NinjaSnowWar implements footstep particle effects using animation triggers.
+
+\code
+<animation>
+    <trigger time="t" normalizedtime="t" type="Int|Bool|Float|String..." value="x" />
+    <trigger ... />
+</animation>
+\endcode
+<particleemitter>
 
 
 \page Particles %Particle systems
 \page Particles %Particle systems
 
 
@@ -710,7 +723,7 @@ The ParticleEmitter class derives from BillboardSet to implement a particle syst
 
 
 The particle system's properties can be set through a XML description file, see \ref ParticleEmitter::LoadParameters "LoadParameters()".
 The particle system's properties can be set through a XML description file, see \ref ParticleEmitter::LoadParameters "LoadParameters()".
 
 
-Most of the parameters can take either a single value, or minimum and maximum values to allow for random variation. See below for all supported parameters:s
+Most of the parameters can take either a single value, or minimum and maximum values to allow for random variation. See below for all supported parameters:
 
 
 \code
 \code
 <particleemitter>
 <particleemitter>

+ 5 - 0
Docs/ScriptAPI.dox

@@ -53,6 +53,7 @@
 - String GetFileName(const String&)
 - String GetFileName(const String&)
 - String GetExtension(const String&)
 - String GetExtension(const String&)
 - String GetFileNameAndExtension(const String&)
 - String GetFileNameAndExtension(const String&)
+- String ReplaceExtension(const String&)
 - String AddTrailingSlash(const String&)
 - String AddTrailingSlash(const String&)
 - String RemoveTrailingSlash(const String&)
 - String RemoveTrailingSlash(const String&)
 - String GetParentPath(const String&)
 - String GetParentPath(const String&)
@@ -1781,6 +1782,9 @@ Animation
 Methods:<br>
 Methods:<br>
 - bool Load(File@)
 - bool Load(File@)
 - bool Save(File@)
 - bool Save(File@)
+- void AddTrigger(float, bool, const Variant&)
+- void RemoveTrigger(uint)
+- void RemoveAllTriggers()
 
 
 Properties:<br>
 Properties:<br>
 - ShortStringHash type (readonly)
 - ShortStringHash type (readonly)
@@ -1791,6 +1795,7 @@ Properties:<br>
 - String animationName (readonly)
 - String animationName (readonly)
 - float length (readonly)
 - float length (readonly)
 - uint numTracks (readonly)
 - uint numTracks (readonly)
+- uint numTriggers (readonly)
 
 
 
 
 Drawable
 Drawable

+ 1 - 4
Engine/Audio/Sound.cpp

@@ -373,10 +373,7 @@ unsigned Sound::GetSampleSize() const
 void Sound::LoadParameters()
 void Sound::LoadParameters()
 {
 {
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     ResourceCache* cache = GetSubsystem<ResourceCache>();
-    
-    String soundPath, soundName, soundExt;
-    SplitPath(GetName(), soundPath, soundName, soundExt);
-    String xmlName = soundPath + soundName + ".xml";
+    String xmlName = ReplaceExtension(GetName(), ".xml");
     
     
     if (!cache->Exists(xmlName))
     if (!cache->Exists(xmlName))
         return;
         return;

+ 4 - 0
Engine/Engine/GraphicsAPI.cpp

@@ -448,8 +448,12 @@ static void RegisterAnimation(asIScriptEngine* engine)
 {
 {
     RegisterResource<Animation>(engine, "Animation");
     RegisterResource<Animation>(engine, "Animation");
     engine->RegisterObjectMethod("Animation", "const String& get_animationName() const", asMETHOD(Animation, GetAnimationName), asCALL_THISCALL);
     engine->RegisterObjectMethod("Animation", "const String& get_animationName() const", asMETHOD(Animation, GetAnimationName), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Animation", "void AddTrigger(float, bool, const Variant&in)", asMETHOD(Animation, AddTrigger), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Animation", "void RemoveTrigger(uint)", asMETHOD(Animation, RemoveTrigger), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Animation", "void RemoveAllTriggers()", asMETHOD(Animation, RemoveAllTriggers), asCALL_THISCALL);
     engine->RegisterObjectMethod("Animation", "float get_length() const", asMETHOD(Animation, GetLength), asCALL_THISCALL);
     engine->RegisterObjectMethod("Animation", "float get_length() const", asMETHOD(Animation, GetLength), asCALL_THISCALL);
     engine->RegisterObjectMethod("Animation", "uint get_numTracks() const", asMETHOD(Animation, GetNumTracks), asCALL_THISCALL);
     engine->RegisterObjectMethod("Animation", "uint get_numTracks() const", asMETHOD(Animation, GetNumTracks), asCALL_THISCALL);
+    engine->RegisterObjectMethod("Animation", "uint get_numTriggers() const", asMETHOD(Animation, GetNumTriggers), asCALL_THISCALL);
 }
 }
 
 
 static void RegisterDrawable(asIScriptEngine* engine)
 static void RegisterDrawable(asIScriptEngine* engine)

+ 1 - 0
Engine/Engine/IOAPI.cpp

@@ -295,6 +295,7 @@ void RegisterFileSystem(asIScriptEngine* engine)
     engine->RegisterGlobalFunction("String GetFileName(const String&in)", asFUNCTION(GetFileName), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetFileName(const String&in)", asFUNCTION(GetFileName), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetExtension(const String&in)", asFUNCTION(GetExtension), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetExtension(const String&in)", asFUNCTION(GetExtension), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetFileNameAndExtension(const String&in)", asFUNCTION(GetFileNameAndExtension), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetFileNameAndExtension(const String&in)", asFUNCTION(GetFileNameAndExtension), asCALL_CDECL);
+    engine->RegisterGlobalFunction("String ReplaceExtension(const String&in)", asFUNCTION(ReplaceExtension), asCALL_CDECL);
     engine->RegisterGlobalFunction("String AddTrailingSlash(const String&in)", asFUNCTION(AddTrailingSlash), asCALL_CDECL);
     engine->RegisterGlobalFunction("String AddTrailingSlash(const String&in)", asFUNCTION(AddTrailingSlash), asCALL_CDECL);
     engine->RegisterGlobalFunction("String RemoveTrailingSlash(const String&in)", asFUNCTION(RemoveTrailingSlash), asCALL_CDECL);
     engine->RegisterGlobalFunction("String RemoveTrailingSlash(const String&in)", asFUNCTION(RemoveTrailingSlash), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetParentPath(const String&in)", asFUNCTION(GetParentPath), asCALL_CDECL);
     engine->RegisterGlobalFunction("String GetParentPath(const String&in)", asFUNCTION(GetParentPath), asCALL_CDECL);

+ 75 - 2
Engine/Graphics/Animation.cpp

@@ -25,12 +25,20 @@
 #include "Animation.h"
 #include "Animation.h"
 #include "Context.h"
 #include "Context.h"
 #include "Deserializer.h"
 #include "Deserializer.h"
+#include "FileSystem.h"
 #include "Log.h"
 #include "Log.h"
 #include "Profiler.h"
 #include "Profiler.h"
+#include "ResourceCache.h"
 #include "Serializer.h"
 #include "Serializer.h"
+#include "XMLFile.h"
 
 
 #include "DebugNew.h"
 #include "DebugNew.h"
 
 
+inline bool CompareTriggers(AnimationTriggerPoint& lhs, AnimationTriggerPoint& rhs)
+{
+    return lhs.time_ < rhs.time_;
+}
+
 void AnimationTrack::GetKeyFrameIndex(float time, unsigned& index) const
 void AnimationTrack::GetKeyFrameIndex(float time, unsigned& index) const
 {
 {
     if (time < 0.0f)
     if (time < 0.0f)
@@ -113,6 +121,31 @@ bool Animation::Load(Deserializer& source)
         }
         }
     }
     }
     
     
+    // Optionally read triggers from an XML file
+    ResourceCache* cache = GetSubsystem<ResourceCache>();
+    String xmlName = ReplaceExtension(GetName(), ".xml");
+    
+    if (cache->Exists(xmlName))
+    {
+        XMLFile* file = cache->GetResource<XMLFile>(xmlName);
+        if (file)
+        {
+            XMLElement rootElem = file->GetRoot();
+            XMLElement triggerElem = rootElem.GetChild("trigger");
+            while (triggerElem)
+            {
+                if (triggerElem.HasAttribute("normalizedtime"))
+                    AddTrigger(triggerElem.GetFloat("normalizedtime"), true, triggerElem.GetVariant());
+                else if (triggerElem.HasAttribute("time"))
+                    AddTrigger(triggerElem.GetFloat("time"), false, triggerElem.GetVariant());
+                
+                triggerElem = triggerElem.GetNext("trigger");
+            }
+            
+            memoryUse += triggers_.Size() * sizeof(AnimationTriggerPoint);
+        }
+    }
+    
     SetMemoryUse(memoryUse);
     SetMemoryUse(memoryUse);
     return true;
     return true;
 }
 }
@@ -147,6 +180,30 @@ bool Animation::Save(Serializer& dest)
         }
         }
     }
     }
     
     
+    // If triggers have been defined, write an XML file for them
+    if (triggers_.Size())
+    {
+        File* destFile = dynamic_cast<File*>(&dest);
+        if (destFile)
+        {
+            String xmlName = ReplaceExtension(destFile->GetName(), ".xml");
+            
+            SharedPtr<XMLFile> xml(new XMLFile(context_));
+            XMLElement rootElem = xml->CreateRoot("animation");
+            
+            for (unsigned i = 0; i < triggers_.Size(); ++i)
+            {
+                XMLElement triggerElem = rootElem.CreateChild("trigger");
+                triggerElem.SetFloat("time", triggers_[i].time_);
+                triggerElem.SetVariant(triggers_[i].data_);
+            }
+            
+            xml->Save(File(context_, xmlName, FILE_WRITE));
+        }
+        else
+            LOGWARNING("Can not save animation trigger data when not saving into a file");
+    }
+    
     return true;
     return true;
 }
 }
 
 
@@ -166,9 +223,25 @@ void Animation::SetTracks(const Vector<AnimationTrack>& tracks)
     tracks_ = tracks;
     tracks_ = tracks;
 }
 }
 
 
-unsigned Animation::GetNumTracks() const
+void Animation::AddTrigger(float time, bool timeIsNormalized, const Variant& data)
+{
+    AnimationTriggerPoint newTrigger;
+    newTrigger.time_ = timeIsNormalized ? time * length_ : time;
+    newTrigger.data_ = data;
+    triggers_.Push(newTrigger);
+    
+    Sort(triggers_.Begin(), triggers_.End(), CompareTriggers);
+}
+
+void Animation::RemoveTrigger(unsigned index)
+{
+    if (index < triggers_.Size())
+        triggers_.Erase(index);
+}
+
+void Animation::RemoveAllTriggers()
 {
 {
-    return tracks_.Size();
+    triggers_.Clear();
 }
 }
 
 
 const AnimationTrack* Animation::GetTrack(unsigned index) const
 const AnimationTrack* Animation::GetTrack(unsigned index) const

+ 22 - 1
Engine/Graphics/Animation.h

@@ -57,6 +57,15 @@ struct AnimationTrack
     Vector<AnimationKeyFrame> keyFrames_;
     Vector<AnimationKeyFrame> keyFrames_;
 };
 };
 
 
+/// Animation trigger point.
+struct AnimationTriggerPoint
+{
+    /// Trigger time.
+    float time_;
+    /// Trigger data.
+    Variant data_;
+};
+
 static const unsigned char CHANNEL_POSITION = 0x1;
 static const unsigned char CHANNEL_POSITION = 0x1;
 static const unsigned char CHANNEL_ROTATION = 0x2;
 static const unsigned char CHANNEL_ROTATION = 0x2;
 static const unsigned char CHANNEL_SCALE = 0x4;
 static const unsigned char CHANNEL_SCALE = 0x4;
@@ -85,6 +94,12 @@ public:
     void SetLength(float length);
     void SetLength(float length);
     /// Set all animation tracks.
     /// Set all animation tracks.
     void SetTracks(const Vector<AnimationTrack>& tracks);
     void SetTracks(const Vector<AnimationTrack>& tracks);
+    /// Add a trigger point.
+    void AddTrigger(float time, bool timeIsNormalized, const Variant& data);
+    /// Remove a trigger point by index.
+    void RemoveTrigger(unsigned index);
+    /// Remove all trigger points.
+    void RemoveAllTriggers();
     
     
     /// Return animation name.
     /// Return animation name.
     const String& GetAnimationName() const { return animationName_; }
     const String& GetAnimationName() const { return animationName_; }
@@ -95,13 +110,17 @@ public:
     /// Return all animation tracks.
     /// Return all animation tracks.
     const Vector<AnimationTrack>& GetTracks() const { return tracks_; }
     const Vector<AnimationTrack>& GetTracks() const { return tracks_; }
     /// Return number of animation tracks.
     /// Return number of animation tracks.
-    unsigned GetNumTracks() const;
+    unsigned GetNumTracks() const { return tracks_.Size(); }
     /// Return animation track by index.
     /// Return animation track by index.
     const AnimationTrack* GetTrack(unsigned index) const;
     const AnimationTrack* GetTrack(unsigned index) const;
     /// Return animation track by bone name.
     /// Return animation track by bone name.
     const AnimationTrack* GetTrack(const String& name) const;
     const AnimationTrack* GetTrack(const String& name) const;
     /// Return animation track by bone name hash.
     /// Return animation track by bone name hash.
     const AnimationTrack* GetTrack(StringHash nameHash) const;
     const AnimationTrack* GetTrack(StringHash nameHash) const;
+    /// Return animation trigger points.
+    const Vector<AnimationTriggerPoint>& GetTriggers() const { return triggers_; }
+    /// Return number of animation trigger points.
+    unsigned GetNumTriggers() const {return triggers_.Size(); }
     
     
 private:
 private:
     /// Animation name.
     /// Animation name.
@@ -112,4 +131,6 @@ private:
     float length_;
     float length_;
     /// Animation tracks.
     /// Animation tracks.
     Vector<AnimationTrack> tracks_;
     Vector<AnimationTrack> tracks_;
+    /// Animation trigger points.
+    Vector<AnimationTriggerPoint> triggers_;
 };
 };

+ 36 - 1
Engine/Graphics/AnimationState.cpp

@@ -26,6 +26,7 @@
 #include "Animation.h"
 #include "Animation.h"
 #include "AnimationState.h"
 #include "AnimationState.h"
 #include "Deserializer.h"
 #include "Deserializer.h"
+#include "DrawableEvents.h"
 #include "Serializer.h"
 #include "Serializer.h"
 #include "XMLElement.h"
 #include "XMLElement.h"
 
 
@@ -139,7 +140,8 @@ void AnimationState::AddTime(float delta)
     if (delta == 0.0f || length == 0.0f)
     if (delta == 0.0f || length == 0.0f)
         return;
         return;
     
     
-    float time = GetTime() + delta;
+    float oldTime = GetTime();
+    float time = oldTime + delta;
     if (looped_)
     if (looped_)
     {
     {
         while (time >= length)
         while (time >= length)
@@ -149,6 +151,39 @@ void AnimationState::AddTime(float delta)
     }
     }
     
     
     SetTime(time);
     SetTime(time);
+    
+    // Process animation triggers
+    if (model_ && animation_->GetNumTriggers())
+    {
+        if (delta > 0.0f)
+        {
+            if (oldTime > time)
+                oldTime -= length;
+        }
+        if (delta < 0.0f)
+        {
+            if (time > oldTime)
+                time -= length;
+        }
+        if (oldTime > time)
+            Swap(oldTime, time);
+        
+        const Vector<AnimationTriggerPoint>& triggers = animation_->GetTriggers();
+        for (Vector<AnimationTriggerPoint>::ConstIterator i = triggers.Begin(); i != triggers.End(); ++i)
+        {
+            if (oldTime <= i->time_ && time > i->time_)
+            {
+                using namespace AnimationTrigger;
+                
+                VariantMap eventData;
+                eventData[P_NODE] = (void*)model_->GetNode();
+                eventData[P_NAME] = animation_->GetAnimationName();
+                eventData[P_TIME] = i->time_;
+                eventData[P_DATA] = i->data_;
+                model_->GetNode()->SendEvent(E_ANIMATIONTRIGGER, eventData);
+            }
+        }
+    }
 }
 }
 
 
 void AnimationState::SetLayer(unsigned char layer)
 void AnimationState::SetLayer(unsigned char layer)

+ 2 - 2
Engine/Graphics/AnimationState.h

@@ -49,11 +49,11 @@ public:
     void SetLooped(bool looped);
     void SetLooped(bool looped);
     /// Set blending weight.
     /// Set blending weight.
     void SetWeight(float weight);
     void SetWeight(float weight);
-    /// Set time position.
+    /// Set time position. Does not fire animation triggers.
     void SetTime(float time);
     void SetTime(float time);
     /// Modify blending weight.
     /// Modify blending weight.
     void AddWeight(float delta);
     void AddWeight(float delta);
-    /// Modify time position.
+    /// Modify time position. Animation triggers will be fired.
     void AddTime(float delta);
     void AddTime(float delta);
     /// Set blending layer.
     /// Set blending layer.
     void SetLayer(unsigned char layer);
     void SetLayer(unsigned char layer);

+ 2 - 1
Engine/Graphics/Direct3D9/D3D9Shader.cpp

@@ -85,7 +85,8 @@ bool Shader::Load(Deserializer& source)
     {
     {
         ResourceCache* cache = GetSubsystem<ResourceCache>();
         ResourceCache* cache = GetSubsystem<ResourceCache>();
         FileSystem* fileSystem = GetSubsystem<FileSystem>();
         FileSystem* fileSystem = GetSubsystem<FileSystem>();
-        if (cache && fileSystem && !fileSystem->HasRegisteredPaths())
+        
+        if (fileSystem && !fileSystem->HasRegisteredPaths())
         {
         {
             fullFileName_ = cache->GetResourceFileName(GetName());
             fullFileName_ = cache->GetResourceFileName(GetName());
             if (!fullFileName_.Empty())
             if (!fullFileName_.Empty())

+ 1 - 4
Engine/Graphics/Direct3D9/D3D9Texture.cpp

@@ -182,10 +182,7 @@ unsigned Texture::GetRowDataSize(int width) const
 void Texture::LoadParameters()
 void Texture::LoadParameters()
 {
 {
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     ResourceCache* cache = GetSubsystem<ResourceCache>();
-    
-    String texPath, texName, texExt;
-    SplitPath(GetName(), texPath, texName, texExt);
-    String xmlName = texPath + texName + ".xml";
+    String xmlName = ReplaceExtension(GetName(), ".xml");
     
     
     if (cache->Exists(xmlName))
     if (cache->Exists(xmlName))
     {
     {

+ 8 - 0
Engine/Graphics/DrawableEvents.h

@@ -31,6 +31,14 @@ EVENT(E_BONEHIERARCHYCREATED, BoneHierarchyCreated)
     PARAM(P_NODE, Node);                    // Node pointer
     PARAM(P_NODE, Node);                    // Node pointer
 }
 }
 
 
+/// AnimatedModel animation trigger.
+EVENT(E_ANIMATIONTRIGGER, AnimationTrigger)
+{
+    PARAM(P_NODE, Node);                    // Node pointer
+    PARAM(P_NAME, Name);                    // String
+    PARAM(P_TIME, Time);                    // Float
+    PARAM(P_DATA, Data);                    // User-defined data type
+}
 /// Terrain geometry created.
 /// Terrain geometry created.
 EVENT(E_TERRAINCREATED, TerrainCreated)
 EVENT(E_TERRAINCREATED, TerrainCreated)
 {
 {

+ 1 - 4
Engine/Graphics/OpenGL/OGLTexture.cpp

@@ -325,10 +325,7 @@ unsigned Texture::GetDataType(unsigned format)
 void Texture::LoadParameters()
 void Texture::LoadParameters()
 {
 {
     ResourceCache* cache = GetSubsystem<ResourceCache>();
     ResourceCache* cache = GetSubsystem<ResourceCache>();
-    
-    String texPath, texName, texExt;
-    SplitPath(GetName(), texPath, texName, texExt);
-    String xmlName = texPath + texName + ".xml";
+    String xmlName = ReplaceExtension(GetName(), ".xml");
     
     
     if (cache->Exists(xmlName))
     if (cache->Exists(xmlName))
     {
     {

+ 7 - 0
Engine/IO/FileSystem.cpp

@@ -606,6 +606,13 @@ String GetFileNameAndExtension(const String& fileName)
     return file + extension;
     return file + extension;
 }
 }
 
 
+String ReplaceExtension(const String& fullPath, const String& newExtension)
+{
+    String path, file, extension;
+    SplitPath(fullPath, path, file, extension);
+    return path + file + newExtension;
+}
+
 String AddTrailingSlash(const String& pathName)
 String AddTrailingSlash(const String& pathName)
 {
 {
     String ret = pathName;
     String ret = pathName;

+ 2 - 0
Engine/IO/FileSystem.h

@@ -100,6 +100,8 @@ String GetFileName(const String& fullPath);
 String GetExtension(const String& fullPath);
 String GetExtension(const String& fullPath);
 /// Return the filename and extension from a full path. The extension will be converted to lowercase.
 /// Return the filename and extension from a full path. The extension will be converted to lowercase.
 String GetFileNameAndExtension(const String& fullPath);
 String GetFileNameAndExtension(const String& fullPath);
+/// Replace the extension of a file name with another.
+String ReplaceExtension(const String& fullPath, const String& newExtension);
 /// Add a slash at the end of the path if missing and convert to internal format (use slashes.)
 /// Add a slash at the end of the path if missing and convert to internal format (use slashes.)
 String AddTrailingSlash(const String& pathName);
 String AddTrailingSlash(const String& pathName);
 /// Remove the slash from the end of a path if exists and convert to internal format (use slashes.)
 /// Remove the slash from the end of a path if exists and convert to internal format (use slashes.)