Now root motion works much better; raft/boat climbing
This commit is contained in:
@@ -3,9 +3,74 @@
|
||||
#include <Ogre.h>
|
||||
#include <flecs.h>
|
||||
#include "GameData.h"
|
||||
#include "CharacterModule.h"
|
||||
#include "LuaData.h"
|
||||
namespace ECS
|
||||
{
|
||||
class RootMotionListener : public Ogre::NodeAnimationTrack::Listener {
|
||||
Ogre::Vector3 prevTranslation;
|
||||
mutable Ogre::Vector3 deltaMotion;
|
||||
flecs::entity e;
|
||||
|
||||
public:
|
||||
RootMotionListener(flecs::entity e)
|
||||
: Ogre::NodeAnimationTrack::Listener()
|
||||
, e(e)
|
||||
, prevTranslation(Ogre::Vector3::ZERO)
|
||||
, deltaMotion(Ogre::Vector3::ZERO)
|
||||
{
|
||||
}
|
||||
bool getInterpolatedKeyFrame(const Ogre::AnimationTrack *t,
|
||||
const Ogre::TimeIndex &timeIndex,
|
||||
Ogre::KeyFrame *kf) override
|
||||
{
|
||||
Ogre::TransformKeyFrame *vkf =
|
||||
static_cast<Ogre::TransformKeyFrame *>(kf);
|
||||
Ogre::KeyFrame *kf1, *kf2;
|
||||
Ogre::TransformKeyFrame *k1, *k2;
|
||||
unsigned short firstKeyIndex;
|
||||
float tm = t->getKeyFramesAtTime(timeIndex, &kf1, &kf2,
|
||||
&firstKeyIndex);
|
||||
k1 = static_cast<Ogre::TransformKeyFrame *>(kf1);
|
||||
k2 = static_cast<Ogre::TransformKeyFrame *>(kf2);
|
||||
Ogre::Vector3 translation;
|
||||
Ogre::Quaternion rotation;
|
||||
if (tm == 0.0f) {
|
||||
rotation = k1->getRotation();
|
||||
translation = k1->getTranslate();
|
||||
deltaMotion = translation;
|
||||
|
||||
// vkf->setRotation(k1->getRotation());
|
||||
// vkf->setTranslate(k1->getTranslate());
|
||||
// vkf->setScale(k1->getScale());
|
||||
} else {
|
||||
rotation = Ogre::Quaternion::nlerp(
|
||||
tm, k1->getRotation(), k2->getRotation(), true);
|
||||
translation =
|
||||
k1->getTranslate() +
|
||||
(k2->getTranslate() - k1->getTranslate()) * tm;
|
||||
deltaMotion = translation - prevTranslation;
|
||||
if (deltaMotion.squaredLength() >
|
||||
translation.squaredLength())
|
||||
deltaMotion = translation;
|
||||
}
|
||||
#if 0
|
||||
std::cout << "time: " << tm
|
||||
<< " Position: " << deltaMotion;
|
||||
std::cout << " Quaternion: " << rotation;
|
||||
std::cout << std::endl;
|
||||
#endif
|
||||
vkf->setTranslate(deltaMotion);
|
||||
// vkf->setTranslate(translation);
|
||||
vkf->setRotation(rotation);
|
||||
vkf->setScale(Ogre::Vector3(1, 1, 1));
|
||||
prevTranslation = translation;
|
||||
e.get_mut<CharacterBase>().mBoneMotion = deltaMotion;
|
||||
e.get_mut<CharacterBase>().mBonePrevMotion = prevTranslation;
|
||||
e.modified<CharacterBase>();
|
||||
return true;
|
||||
}
|
||||
};
|
||||
struct AnimationTrigger;
|
||||
struct AnimationTriggerSubscriber {
|
||||
virtual void operator()(const AnimationTrigger *trigger) = 0;
|
||||
@@ -65,17 +130,20 @@ struct Animation {
|
||||
Ogre::Animation *mSkelAnimation;
|
||||
Ogre::NodeAnimationTrack *mHipsTrack;
|
||||
Ogre::NodeAnimationTrack *mRootTrack;
|
||||
RootMotionListener *mListener;
|
||||
float m_weight;
|
||||
float m_accWeight;
|
||||
std::multimap<float, AnimationTrigger *> trigger_list;
|
||||
Animation(Ogre::AnimationState *animState,
|
||||
Ogre::Animation *skelAnimation)
|
||||
Animation(Ogre::Skeleton *skeleton, Ogre::AnimationState *animState,
|
||||
Ogre::Animation *skelAnimation, flecs::entity e)
|
||||
: mAnimationState(animState)
|
||||
, mSkelAnimation(skelAnimation)
|
||||
, mListener(OGRE_NEW RootMotionListener(e))
|
||||
, m_weight(0)
|
||||
, m_accWeight(0)
|
||||
{
|
||||
int j;
|
||||
mRootTrack = nullptr;
|
||||
mHipsTrack = nullptr;
|
||||
for (const auto &it : mSkelAnimation->_getNodeTrackList()) {
|
||||
Ogre::NodeAnimationTrack *track = it.second;
|
||||
Ogre::String trackName =
|
||||
@@ -87,10 +155,21 @@ struct Animation {
|
||||
// mRootTracks[i]->removeAllKeyFrames();
|
||||
}
|
||||
}
|
||||
if (!mRootTrack) {
|
||||
Ogre::Bone *bone = skeleton->getBone("Root");
|
||||
mRootTrack = mSkelAnimation->createNodeTrack(
|
||||
bone->getHandle(), bone);
|
||||
Ogre::TransformKeyFrame *kf =
|
||||
mRootTrack->createNodeKeyFrame(0.0f);
|
||||
kf->setTranslate(Ogre::Vector3::ZERO);
|
||||
kf->setRotation(Ogre::Quaternion::IDENTITY);
|
||||
}
|
||||
mRootTrack->setListener(mListener);
|
||||
#if 0
|
||||
OgreAssert(mHipsTrack, "no hips track");
|
||||
OgreAssert(mRootTrack, "no Root track");
|
||||
#endif
|
||||
#if 0
|
||||
if (mRootTrack) {
|
||||
Ogre::Vector3 delta = Ogre::Vector3::ZERO;
|
||||
Ogre::Vector3 motion = Ogre::Vector3::ZERO;
|
||||
@@ -107,6 +186,55 @@ struct Animation {
|
||||
motion = trans;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if 0
|
||||
if (!mMetaRootTrack) {
|
||||
Ogre::Bone *bone = nullptr;
|
||||
OgreAssert(skeleton->hasBone("MetaRoot"),
|
||||
"no bone MetaRoot");
|
||||
if (skeleton->hasBone("MetaRoot"))
|
||||
bone = skeleton->getBone("MetaRoot");
|
||||
#if 0
|
||||
else
|
||||
bone = skeleton->createBone("MetaRoot");
|
||||
#endif
|
||||
bone->setPosition(Ogre::Vector3::ZERO);
|
||||
bone->setOrientation(Ogre::Quaternion::IDENTITY);
|
||||
std::vector<
|
||||
std::pair<float, std::pair<Ogre::Vector3,
|
||||
Ogre::Quaternion> > >
|
||||
keyframes;
|
||||
for (j = 0; j < mRootTrack->getNumKeyFrames(); j++) {
|
||||
Ogre::TransformKeyFrame *kf =
|
||||
mRootTrack->getNodeKeyFrame(j);
|
||||
const Ogre::Vector3 &pt = kf->getTranslate();
|
||||
const Ogre::Quaternion &rt = kf->getRotation();
|
||||
float tp = kf->getTime();
|
||||
keyframes.push_back({ tp, { pt, rt } });
|
||||
#if 0
|
||||
new_kf->setTranslate(pt);
|
||||
new_kf->setTranslate(kf->getTranslate());
|
||||
new_kf->setRotation(kf->getRotation());
|
||||
#endif
|
||||
kf->setTranslate(Ogre::Vector3::ZERO);
|
||||
kf->setRotation(Ogre::Quaternion::IDENTITY);
|
||||
}
|
||||
mMetaRootTrack = mSkelAnimation->createNodeTrack(
|
||||
bone->getHandle());
|
||||
OgreAssert(mMetaRootTrack,
|
||||
"failed to create node track");
|
||||
for (j = 0; j < keyframes.size(); j++) {
|
||||
Ogre::TransformKeyFrame *new_kf =
|
||||
mMetaRootTrack->createNodeKeyFrame(
|
||||
keyframes[j].first);
|
||||
new_kf->setTranslate(keyframes[j].second.first);
|
||||
new_kf->setRotation(keyframes[j].second.second);
|
||||
}
|
||||
}
|
||||
#if 0
|
||||
mRootTrack = track;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
Ogre::String getName()
|
||||
{
|
||||
@@ -139,11 +267,22 @@ struct Animation {
|
||||
{
|
||||
return m_weight;
|
||||
}
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
preUpdateTriggers();
|
||||
bool result = mAnimationState->getEnabled();
|
||||
if (!result)
|
||||
return result;
|
||||
Ogre::TimeIndex index = mSkelAnimation->_getTimeIndex(
|
||||
mAnimationState->getTimePosition());
|
||||
Ogre::KeyFrame *kf1, *kf2;
|
||||
unsigned short prev_index, next_index;
|
||||
mRootTrack->getKeyFramesAtTime(index, &kf1, &kf2, &prev_index);
|
||||
unsigned int previous_frame = index.getKeyIndex();
|
||||
mAnimationState->addTime(time);
|
||||
postUpdateTriggers(time);
|
||||
index = mSkelAnimation->_getTimeIndex(
|
||||
mAnimationState->getTimePosition());
|
||||
mRootTrack->getKeyFramesAtTime(index, &kf1, &kf2, &next_index);
|
||||
return prev_index != next_index;
|
||||
}
|
||||
void reset()
|
||||
{
|
||||
@@ -161,55 +300,9 @@ struct Animation {
|
||||
{
|
||||
return m_accWeight;
|
||||
}
|
||||
void addTrigger(AnimationTrigger *trigger)
|
||||
{
|
||||
trigger_list.insert(std::pair<float, AnimationTrigger *>(
|
||||
trigger->getTriggerTime(), trigger));
|
||||
}
|
||||
void clearTriggers()
|
||||
{
|
||||
auto it = trigger_list.begin();
|
||||
while (it != trigger_list.end()) {
|
||||
delete it->second;
|
||||
it++;
|
||||
}
|
||||
trigger_list.clear();
|
||||
}
|
||||
float mpreUpdateTime;
|
||||
void preUpdateTriggers()
|
||||
{
|
||||
mpreUpdateTime = mAnimationState->getTimePosition() /
|
||||
mAnimationState->getLength();
|
||||
}
|
||||
void postUpdateTriggers(float delta)
|
||||
{
|
||||
float postUpdateTime = mAnimationState->getTimePosition() /
|
||||
mAnimationState->getLength();
|
||||
bool positive = delta >= 0.0f;
|
||||
if (positive)
|
||||
updateTriggers(mpreUpdateTime, postUpdateTime);
|
||||
else
|
||||
updateTriggers(postUpdateTime, mpreUpdateTime);
|
||||
}
|
||||
void updateTriggers(float currentTime, float nextTime)
|
||||
{
|
||||
int i;
|
||||
float weight = getWeight();
|
||||
if (currentTime <= nextTime) {
|
||||
auto it = trigger_list.lower_bound(currentTime);
|
||||
while (it != trigger_list.end()) {
|
||||
if (nextTime <=
|
||||
it->second->getTriggerTime()) // in future, sorrted by time
|
||||
return;
|
||||
it->second->notify(weight);
|
||||
}
|
||||
} else {
|
||||
updateTriggers(currentTime, 1);
|
||||
updateTriggers(0, nextTime);
|
||||
}
|
||||
}
|
||||
float getLength() const
|
||||
{
|
||||
return mAnimationState->getLength();
|
||||
if (getEnabled())
|
||||
return mAnimationState->getLength();
|
||||
else
|
||||
@@ -217,6 +310,7 @@ struct Animation {
|
||||
}
|
||||
float getTimePosition() const
|
||||
{
|
||||
return mAnimationState->getTimePosition();
|
||||
if (getEnabled())
|
||||
return mAnimationState->getTimePosition();
|
||||
else
|
||||
@@ -228,11 +322,12 @@ struct AnimationNode {
|
||||
std::vector<AnimationNode *> children;
|
||||
float m_weight;
|
||||
Ogre::String m_name;
|
||||
std::multimap<float, AnimationTrigger *> trigger_list;
|
||||
AnimationNode()
|
||||
: m_weight(0)
|
||||
{
|
||||
}
|
||||
virtual void addTime(float time) = 0;
|
||||
virtual bool addTime(float time) = 0;
|
||||
virtual void setWeight(float weight) = 0;
|
||||
virtual void reset() = 0;
|
||||
virtual float getLength() const = 0;
|
||||
@@ -256,6 +351,52 @@ struct AnimationNode {
|
||||
return getTimePosition() / l;
|
||||
return 0.0f;
|
||||
}
|
||||
void addTrigger(AnimationTrigger *trigger)
|
||||
{
|
||||
trigger_list.insert(std::pair<float, AnimationTrigger *>(
|
||||
trigger->getTriggerTime(), trigger));
|
||||
}
|
||||
void clearTriggers()
|
||||
{
|
||||
auto it = trigger_list.begin();
|
||||
while (it != trigger_list.end()) {
|
||||
delete it->second;
|
||||
it++;
|
||||
}
|
||||
trigger_list.clear();
|
||||
}
|
||||
float mpreUpdateTime;
|
||||
void preUpdateTriggers()
|
||||
{
|
||||
mpreUpdateTime = getTime();
|
||||
}
|
||||
void postUpdateTriggers(float delta)
|
||||
{
|
||||
float postUpdateTime = getTime();
|
||||
bool positive = delta >= 0.0f;
|
||||
if (positive)
|
||||
updateTriggers(mpreUpdateTime, postUpdateTime);
|
||||
else
|
||||
updateTriggers(postUpdateTime, mpreUpdateTime);
|
||||
}
|
||||
void updateTriggers(float currentTime, float nextTime)
|
||||
{
|
||||
int i;
|
||||
float weight = getWeight();
|
||||
if (currentTime <= nextTime) {
|
||||
auto it = trigger_list.lower_bound(currentTime);
|
||||
while (it != trigger_list.end()) {
|
||||
if (nextTime <=
|
||||
it->second->getTriggerTime()) // in future, sorrted by time
|
||||
return;
|
||||
it->second->notify(weight);
|
||||
it++;
|
||||
}
|
||||
} else {
|
||||
updateTriggers(currentTime, 1);
|
||||
updateTriggers(0, nextTime);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
struct AnimationNodeAnimation : AnimationNode {
|
||||
@@ -266,9 +407,13 @@ struct AnimationNodeAnimation : AnimationNode {
|
||||
, mAnimation(animation)
|
||||
{
|
||||
}
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
mAnimation->addTime(time);
|
||||
bool ret;
|
||||
preUpdateTriggers();
|
||||
ret = mAnimation->addTime(time);
|
||||
postUpdateTriggers(time);
|
||||
return ret;
|
||||
}
|
||||
void setWeight(float weight)
|
||||
{
|
||||
@@ -281,6 +426,7 @@ struct AnimationNodeAnimation : AnimationNode {
|
||||
}
|
||||
float getLength() const
|
||||
{
|
||||
return mAnimation->getLength();
|
||||
if (enabled)
|
||||
return mAnimation->getLength();
|
||||
else
|
||||
@@ -288,6 +434,7 @@ struct AnimationNodeAnimation : AnimationNode {
|
||||
}
|
||||
float getTimePosition() const
|
||||
{
|
||||
return mAnimation->getTimePosition();
|
||||
if (enabled)
|
||||
return mAnimation->getTimePosition();
|
||||
else
|
||||
@@ -299,9 +446,13 @@ struct AnimationNodeStateMachineState : AnimationNode {
|
||||
: AnimationNode()
|
||||
{
|
||||
}
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
children[0]->addTime(time);
|
||||
bool ret;
|
||||
preUpdateTriggers();
|
||||
ret = children[0]->addTime(time);
|
||||
postUpdateTriggers(time);
|
||||
return ret;
|
||||
}
|
||||
void setWeight(float weight)
|
||||
{
|
||||
@@ -319,7 +470,7 @@ struct AnimationNodeStateMachineState : AnimationNode {
|
||||
}
|
||||
float getTimePosition() const
|
||||
{
|
||||
return children[0]->getLength();
|
||||
return children[0]->getTimePosition();
|
||||
}
|
||||
};
|
||||
struct AnimationNodeSpeed : AnimationNode {
|
||||
@@ -331,9 +482,13 @@ struct AnimationNodeSpeed : AnimationNode {
|
||||
, enabled(false)
|
||||
{
|
||||
}
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
children[0]->addTime(time * m_speed);
|
||||
bool ret;
|
||||
preUpdateTriggers();
|
||||
ret = children[0]->addTime(time * m_speed);
|
||||
postUpdateTriggers(time);
|
||||
return ret;
|
||||
}
|
||||
void setWeight(float weight)
|
||||
{
|
||||
@@ -383,36 +538,43 @@ struct AnimationNodeStateMachine : AnimationNode {
|
||||
{
|
||||
m_weight = 1.0f;
|
||||
}
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
int i;
|
||||
preUpdateTriggers();
|
||||
if (!configured) {
|
||||
configure();
|
||||
configured = true;
|
||||
}
|
||||
#ifdef VDEBUG
|
||||
if (debug) {
|
||||
std::cout << "state machine addTime" << std::endl;
|
||||
std::cout
|
||||
<< "state machine children: " << children.size()
|
||||
<< std::endl;
|
||||
}
|
||||
#endif
|
||||
for (i = 0; i < children.size(); i++) {
|
||||
#ifdef VDEBUG
|
||||
if (debug)
|
||||
std::cout << "child weight: " << i << " "
|
||||
<< children[i]->getWeight()
|
||||
<< std::endl;
|
||||
#endif
|
||||
AnimationNode *child = children[i];
|
||||
if (fade_in.find(child) != fade_in.end()) {
|
||||
Ogre::Real newWeight =
|
||||
child->getWeight() + time * fade_speed;
|
||||
child->setWeight(Ogre::Math::Clamp<Ogre::Real>(
|
||||
newWeight * m_weight, 0, m_weight));
|
||||
#ifdef VDEBUG
|
||||
if (debug) {
|
||||
std::cout << "fade in: " << newWeight
|
||||
<< std::endl;
|
||||
std::cout << "m_weight: " << m_weight
|
||||
<< std::endl;
|
||||
}
|
||||
#endif
|
||||
if (newWeight >= 1)
|
||||
fade_in.erase(child);
|
||||
}
|
||||
@@ -426,8 +588,11 @@ struct AnimationNodeStateMachine : AnimationNode {
|
||||
}
|
||||
}
|
||||
OgreAssert(currentAnim, "bad current anim");
|
||||
bool ret = false;
|
||||
if (currentAnim)
|
||||
currentAnim->addTime(time);
|
||||
ret = currentAnim->addTime(time);
|
||||
postUpdateTriggers(time);
|
||||
return ret;
|
||||
}
|
||||
void setWeight(float weight)
|
||||
{
|
||||
@@ -522,9 +687,13 @@ struct AnimationNodeOutput : AnimationNode {
|
||||
, m_speed(1.0f)
|
||||
{
|
||||
}
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
children[0]->addTime(time * m_speed);
|
||||
bool ret;
|
||||
preUpdateTriggers();
|
||||
ret = children[0]->addTime(time * m_speed);
|
||||
postUpdateTriggers(time);
|
||||
return ret;
|
||||
}
|
||||
void setWeight(float weight)
|
||||
{
|
||||
@@ -614,8 +783,9 @@ struct AnimationSystem : AnimationNode {
|
||||
Ogre::String event;
|
||||
void operator()(const AnimationTrigger *trigger)
|
||||
{
|
||||
ECS::get_mut<LuaData>().call_handler(
|
||||
event, ent, ent);
|
||||
ECS::get_mut<LuaBase>()
|
||||
.mLua->call_handler(event, ent,
|
||||
ent);
|
||||
}
|
||||
EventSubscriber(flecs::entity e,
|
||||
const Ogre::String &event)
|
||||
@@ -625,15 +795,57 @@ struct AnimationSystem : AnimationNode {
|
||||
}
|
||||
};
|
||||
OgreAssert(parent, "bad parent");
|
||||
Animation *animation =
|
||||
static_cast<AnimationNodeAnimation *>(parent)
|
||||
->mAnimation;
|
||||
OgreAssert(animation, "bad animation");
|
||||
AnimationTrigger *trigger =
|
||||
new AnimationTrigger(name, time, 0.1f);
|
||||
EventSubscriber *sub = new EventSubscriber(e, event);
|
||||
trigger->addSubscriber(sub);
|
||||
animation->addTrigger(trigger);
|
||||
parent->addTrigger(trigger);
|
||||
return this;
|
||||
} // leaf too...
|
||||
AnimationSystemBuilder *
|
||||
transition_end(const Ogre::String &state_from,
|
||||
const Ogre::String &state_to)
|
||||
{
|
||||
struct EndTransitionSubscriber
|
||||
: AnimationTriggerSubscriber {
|
||||
AnimationNodeStateMachine *sm;
|
||||
Ogre::String next_state;
|
||||
bool reset;
|
||||
void operator()(const AnimationTrigger *trigger)
|
||||
{
|
||||
sm->setAnimation(next_state, reset);
|
||||
}
|
||||
EndTransitionSubscriber(
|
||||
AnimationNodeStateMachine *sm,
|
||||
const Ogre::String &next_state,
|
||||
bool reset = true)
|
||||
: sm(sm)
|
||||
, next_state(next_state)
|
||||
, reset(reset)
|
||||
{
|
||||
}
|
||||
};
|
||||
OgreAssert(parent, "no parent");
|
||||
AnimationNodeStateMachine *sm =
|
||||
static_cast<AnimationNodeStateMachine *>(
|
||||
parent);
|
||||
OgreAssert(sm, "no state machine");
|
||||
AnimationTrigger *trigger = new AnimationTrigger(
|
||||
"transition:" + state_from + "_" + state_to,
|
||||
0.99f, 0.1f);
|
||||
EndTransitionSubscriber *sub =
|
||||
new EndTransitionSubscriber(sm, state_to);
|
||||
int i;
|
||||
bool ok = false;
|
||||
for (i = 0; i < sm->children.size(); i++) {
|
||||
if (sm->children[i]->getName() == state_from) {
|
||||
trigger->addSubscriber(sub);
|
||||
sm->children[i]->addTrigger(trigger);
|
||||
ok = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
OgreAssert(ok, "Failed to set transition");
|
||||
return this;
|
||||
}
|
||||
AnimationSystemBuilder *speed(float speed,
|
||||
@@ -685,10 +897,11 @@ struct AnimationSystem : AnimationNode {
|
||||
}
|
||||
};
|
||||
AnimationSystemBuilder m_builder;
|
||||
void addTime(float time)
|
||||
bool addTime(float time)
|
||||
{
|
||||
int i;
|
||||
m_builder.animation_nodes[0]->addTime(time);
|
||||
preUpdateTriggers();
|
||||
bool ret = m_builder.animation_nodes[0]->addTime(time);
|
||||
for (i = 0; i < m_builder.animationNodeList.size(); i++) {
|
||||
AnimationNodeAnimation *anim =
|
||||
m_builder.animationNodeList[i];
|
||||
@@ -706,13 +919,17 @@ struct AnimationSystem : AnimationNode {
|
||||
float weight = vanimation_list[i]->getAccWeight();
|
||||
vanimation_list[i]->setWeight(weight);
|
||||
vanimation_list[i]->resetAccWeight();
|
||||
#define VDEBUG
|
||||
#ifdef VDEBUG
|
||||
if (debug)
|
||||
if (debug && vanimation_list[i]->getEnabled())
|
||||
std::cout << i << " animation: "
|
||||
<< vanimation_list[i]->getName()
|
||||
<< " " << weight << std::endl;
|
||||
#endif
|
||||
#undef VDEBUG
|
||||
}
|
||||
postUpdateTriggers(time);
|
||||
return ret;
|
||||
}
|
||||
void setWeight(float weight)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user