- RagdollControl is now called KinematicRagdollControl

- better user API (setKinematicMode(), setRagdollMode(),...)
- You can now enable/disable the control anytime, with no side effects
- added more javadoc

git-svn-id: https://jmonkeyengine.googlecode.com/svn/trunk@7370 75d07b2b-3a1a-0410-a2c5-0572b91ccdca
3.0
rem..om 14 years ago
parent 93d0ed73e3
commit 427163475c
  1. 194
      engine/src/jbullet/com/jme3/bullet/control/KinematicRagdollControl.java
  2. 30
      engine/src/test/jme3test/bullet/TestBoneRagdoll.java

@ -78,6 +78,7 @@ import java.util.logging.Logger;
/**<strong>This control is still a WIP, use it at your own risk</strong><br> /**<strong>This control is still a WIP, use it at your own risk</strong><br>
* To use this control you need a model with an AnimControl and a SkeletonControl.<br> * To use this control you need a model with an AnimControl and a SkeletonControl.<br>
* This should be the case if you imported an animated model from Ogre or blender.<br> * This should be the case if you imported an animated model from Ogre or blender.<br>
* Note enabling/disabling the control add/removes it from the physic space<br>
* <p> * <p>
* This control creates collision shapes for each bones of the skeleton when you call spatial.addControl(ragdollControl). * This control creates collision shapes for each bones of the skeleton when you call spatial.addControl(ragdollControl).
* <ul> * <ul>
@ -88,26 +89,25 @@ import java.util.logging.Logger;
* </ul> * </ul>
*</p> *</p>
*<p> *<p>
*There are 2 behavior for this control : *There are 2 modes for this control :
* <ul> * <ul>
* <li><strong>The kinematic behavior :</strong><br> * <li><strong>The kinematic modes :</strong><br>
* this is the default behavior, this means that the collision shapes of the body are able to interact with physics enabled objects. * this is the default behavior, this means that the collision shapes of the body are able to interact with physics enabled objects.
* in this mode physic shapes follow the moovements of the animated skeleton (for example animated by a key framed animation) * in this mode physic shapes follow the moovements of the animated skeleton (for example animated by a key framed animation)
* this mode is enabled just by enabling the control with setEnabled(true); * this mode is enabled by calling setKinematicMode();
* disabling the control removes it from the phyic space
* </li> * </li>
* <li><strong>The ragdoll behavior :</strong><br> * <li><strong>The ragdoll modes :</strong><br>
* To enable this behavior, you need to call setRagdollEnabled(true) method. * To enable this behavior, you need to call setRagdollMode() method.
* In this mode the charater is entirely controled by physics, so it will fall under the garvity and move if any force is applied to it. * In this mode the charater is entirely controled by physics, so it will fall under the gravity and move if any force is applied to it.
* </li> * </li>
* </ul> * </ul>
*</p> *</p>
* *
* @author Normen Hansen and Rémy Bouquet (Nehon) * @author Normen Hansen and Rémy Bouquet (Nehon)
*/ */
public class RagdollControl implements PhysicsControl, PhysicsCollisionListener { public class KinematicRagdollControl implements PhysicsControl, PhysicsCollisionListener {
protected static final Logger logger = Logger.getLogger(RagdollControl.class.getName()); protected static final Logger logger = Logger.getLogger(KinematicRagdollControl.class.getName());
protected Map<String, PhysicsBoneLink> boneLinks = new HashMap<String, PhysicsBoneLink>(); protected Map<String, PhysicsBoneLink> boneLinks = new HashMap<String, PhysicsBoneLink>();
protected Skeleton skeleton; protected Skeleton skeleton;
protected PhysicsSpace space; protected PhysicsSpace space;
@ -117,7 +117,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
protected float weightThreshold = 1.0f; protected float weightThreshold = 1.0f;
protected Spatial targetModel; protected Spatial targetModel;
protected Vector3f initScale; protected Vector3f initScale;
protected boolean control = false; protected Mode mode = Mode.Kinetmatic;
protected boolean blendedControl = false; protected boolean blendedControl = false;
protected float blendTime = 1.0f; protected float blendTime = 1.0f;
protected float blendStart = 0.0f; protected float blendStart = 0.0f;
@ -131,19 +131,28 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
protected float totalMass = 0; protected float totalMass = 0;
protected boolean added = false; protected boolean added = false;
public RagdollControl() { public enum Mode {
Kinetmatic,
Ragdoll
}
/**
* contruct a KinematicRagdollControl
*/
public KinematicRagdollControl() {
} }
public RagdollControl(float weightThreshold) { public KinematicRagdollControl(float weightThreshold) {
this.weightThreshold = weightThreshold; this.weightThreshold = weightThreshold;
} }
public RagdollControl(RagdollPreset preset, float weightThreshold) { public KinematicRagdollControl(RagdollPreset preset, float weightThreshold) {
this.preset = preset; this.preset = preset;
this.weightThreshold = weightThreshold; this.weightThreshold = weightThreshold;
} }
public RagdollControl(RagdollPreset preset) { public KinematicRagdollControl(RagdollPreset preset) {
this.preset = preset; this.preset = preset;
} }
@ -157,7 +166,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
Quaternion tmpRot2 = vars.quat2; Quaternion tmpRot2 = vars.quat2;
//if the ragdoll has the control of the skeleton, we update each bone with its position in physic world space. //if the ragdoll has the control of the skeleton, we update each bone with its position in physic world space.
if (control) { if (mode == mode.Ragdoll) {
for (PhysicsBoneLink link : boneLinks.values()) { for (PhysicsBoneLink link : boneLinks.values()) {
Vector3f position = vars.vect1; Vector3f position = vars.vect1;
@ -205,10 +214,6 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
//the ragdoll does not have the controll, so the keyframed animation updates the physic position of the physic bonces //the ragdoll does not have the controll, so the keyframed animation updates the physic position of the physic bonces
for (PhysicsBoneLink link : boneLinks.values()) { for (PhysicsBoneLink link : boneLinks.values()) {
if (!link.rigidBody.isKinematic()) {
link.rigidBody.setKinematic(true);
}
Vector3f position = vars.vect1; Vector3f position = vars.vect1;
//if blended control this means, keyframed animation is updating the skeleton, //if blended control this means, keyframed animation is updating the skeleton,
@ -237,18 +242,9 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
} }
} }
//setting skeleton transforms to the ragdoll
matchPhysicObjectToBone(link, position, tmpRot1);
//computing position from rotation and scale
targetModel.getWorldTransform().transformVector(link.bone.getModelSpacePosition(), position);
//computing rotation
tmpRot1.set(link.bone.getModelSpaceRotation()).multLocal(link.bone.getWorldBindInverseRotation());
targetModel.getWorldRotation().mult(tmpRot1, tmpRot1);
tmpRot1.normalizeLocal();
//updating physic location/rotation of the physic bone
link.rigidBody.setPhysicsLocation(position);
link.rigidBody.setPhysicsRotation(tmpRot1);
} }
//time control for blending //time control for blending
@ -293,10 +289,40 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
} }
/**
* Set the transforms of a rigidBody to match the transforms of a bone.
* this is used to make the ragdoll follow the skeleton motion while in Kinematic mode
* @param link the link containing the bone and the rigidBody
* @param position just a temp vector for position
* @param tmpRot1 just a temp quaternion for rotation
*/
private void matchPhysicObjectToBone(PhysicsBoneLink link, Vector3f position, Quaternion tmpRot1) {
//computing position from rotation and scale
targetModel.getWorldTransform().transformVector(link.bone.getModelSpacePosition(), position);
//computing rotation
tmpRot1.set(link.bone.getModelSpaceRotation()).multLocal(link.bone.getWorldBindInverseRotation());
targetModel.getWorldRotation().mult(tmpRot1, tmpRot1);
tmpRot1.normalizeLocal();
//updating physic location/rotation of the physic bone
link.rigidBody.setPhysicsLocation(position);
link.rigidBody.setPhysicsRotation(tmpRot1);
// position.set(link.bone.getModelSpaceScale()).multLocal(targetModel.getWorldScale());
// //TODO support scale!
// link.rigidBody.getCollisionShape().setScale(position);
}
public Control cloneForSpatial(Spatial spatial) { public Control cloneForSpatial(Spatial spatial) {
throw new UnsupportedOperationException("Not supported yet."); throw new UnsupportedOperationException("Not supported yet.");
} }
public void reInit() {
setSpatial(targetModel);
addToPhysicsSpace();
}
public void setSpatial(Spatial model) { public void setSpatial(Spatial model) {
if (model == null) { if (model == null) {
removeFromPhysicsSpace(); removeFromPhysicsSpace();
@ -313,7 +339,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
model.removeFromParent(); model.removeFromParent();
model.setLocalTranslation(Vector3f.ZERO); model.setLocalTranslation(Vector3f.ZERO);
model.setLocalRotation(Quaternion.ZERO); model.setLocalRotation(Quaternion.IDENTITY);
model.setLocalScale(1); model.setLocalScale(1);
//HACK ALERT change this //HACK ALERT change this
//I remove the skeletonControl and readd it to the spatial to make sure it's after the ragdollControl in the stack //I remove the skeletonControl and readd it to the spatial to make sure it's after the ragdollControl in the stack
@ -355,11 +381,10 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
if (childBone.getParent() == null) { if (childBone.getParent() == null) {
logger.log(Level.INFO, "Found root bone in skeleton {0}", skeleton); logger.log(Level.INFO, "Found root bone in skeleton {0}", skeleton);
baseRigidBody = new PhysicsRigidBody(new BoxCollisionShape(Vector3f.UNIT_XYZ.mult(0.1f)), 1); baseRigidBody = new PhysicsRigidBody(new BoxCollisionShape(Vector3f.UNIT_XYZ.mult(0.1f)), 1);
baseRigidBody.setKinematic(mode == Mode.Kinetmatic);
boneRecursion(model, childBone, baseRigidBody, 1); boneRecursion(model, childBone, baseRigidBody, 1);
} }
} }
} }
private void boneRecursion(Spatial model, Bone bone, PhysicsRigidBody parent, int reccount) { private void boneRecursion(Spatial model, Bone bone, PhysicsRigidBody parent, int reccount) {
@ -370,6 +395,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
link.bone = bone; link.bone = bone;
//creating the collision shape from the bone's associated vertices //creating the collision shape from the bone's associated vertices
PhysicsRigidBody shapeNode = new PhysicsRigidBody(makeShape(link, model), rootMass / (float) reccount); PhysicsRigidBody shapeNode = new PhysicsRigidBody(makeShape(link, model), rootMass / (float) reccount);
shapeNode.setKinematic(mode == Mode.Kinetmatic);
totalMass += rootMass / (float) reccount; totalMass += rootMass / (float) reccount;
link.rigidBody = shapeNode; link.rigidBody = shapeNode;
@ -458,7 +484,9 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
} }
private void addToPhysicsSpace() { private void addToPhysicsSpace() {
if (space == null) {
return;
}
if (baseRigidBody != null) { if (baseRigidBody != null) {
space.add(baseRigidBody); space.add(baseRigidBody);
added = true; added = true;
@ -467,17 +495,13 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
PhysicsBoneLink physicsBoneLink = it.next(); PhysicsBoneLink physicsBoneLink = it.next();
if (physicsBoneLink.rigidBody != null) { if (physicsBoneLink.rigidBody != null) {
space.add(physicsBoneLink.rigidBody); space.add(physicsBoneLink.rigidBody);
if (physicsBoneLink.joint != null) {
space.add(physicsBoneLink.joint);
}
added = true; added = true;
} }
} }
for (Iterator<PhysicsBoneLink> it = boneLinks.values().iterator(); it.hasNext();) {
PhysicsBoneLink physicsBoneLink = it.next();
if (physicsBoneLink.joint != null) {
space.add(physicsBoneLink.joint);
added = true;
}
}
} }
/** /**
@ -520,6 +544,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
p[i] = points.get(i); p[i] = points.get(i);
} }
return new HullCollisionShape(p); return new HullCollisionShape(p);
} }
@ -585,7 +610,9 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
} }
protected void removeFromPhysicsSpace() { protected void removeFromPhysicsSpace() {
if (space == null) {
return;
}
if (baseRigidBody != null) { if (baseRigidBody != null) {
space.remove(baseRigidBody); space.remove(baseRigidBody);
} }
@ -593,12 +620,9 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
PhysicsBoneLink physicsBoneLink = it.next(); PhysicsBoneLink physicsBoneLink = it.next();
if (physicsBoneLink.joint != null) { if (physicsBoneLink.joint != null) {
space.remove(physicsBoneLink.joint); space.remove(physicsBoneLink.joint);
} if (physicsBoneLink.rigidBody != null) {
} space.remove(physicsBoneLink.rigidBody);
for (Iterator<PhysicsBoneLink> it = boneLinks.values().iterator(); it.hasNext();) { }
PhysicsBoneLink physicsBoneLink = it.next();
if (physicsBoneLink.rigidBody != null) {
space.remove(physicsBoneLink.rigidBody);
} }
} }
added = false; added = false;
@ -606,7 +630,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
/** /**
* enable or disable the control * enable or disable the control
* note that if enabled is true and that the physic space has been set on the ragdoll, the ragdoll is added to the physic space * note that if enabled is true and that the physic space has been set on the ragdoll, the ragdoll is added to the physic space
* if enabled is false the ragdoll is removed from physic space. * if enabled is false the ragdoll is removed from physic space.
* @param enabled * @param enabled
*/ */
@ -723,7 +747,7 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
PhysicsCollisionObject objA = event.getObjectA(); PhysicsCollisionObject objA = event.getObjectA();
PhysicsCollisionObject objB = event.getObjectB(); PhysicsCollisionObject objB = event.getObjectB();
//excluding collisions that do not involve the ragdoll //excluding collisions that involve 2 parts of the ragdoll
if (event.getNodeA() == null && event.getNodeB() == null) { if (event.getNodeA() == null && event.getNodeB() == null) {
return; return;
} }
@ -773,40 +797,71 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
* but will be able to physically interact with its physic environnement * but will be able to physically interact with its physic environnement
* @param ragdollEnabled * @param ragdollEnabled
*/ */
public void setRagdollEnabled(boolean ragdollEnabled) { protected void setMode(Mode mode) {
this.mode = mode;
AnimControl animControl = targetModel.getControl(AnimControl.class); AnimControl animControl = targetModel.getControl(AnimControl.class);
animControl.setEnabled(!ragdollEnabled); animControl.setEnabled(mode == Mode.Kinetmatic);
this.control = ragdollEnabled; baseRigidBody.setKinematic(mode == Mode.Kinetmatic);
TempVars vars = TempVars.get();
assert vars.lock();
for (PhysicsBoneLink link : boneLinks.values()) { for (PhysicsBoneLink link : boneLinks.values()) {
link.rigidBody.setKinematic(!ragdollEnabled); link.rigidBody.setKinematic(mode == Mode.Kinetmatic);
if (mode == Mode.Ragdoll) {
Quaternion tmpRot1 = vars.quat1;
Vector3f position = vars.vect1;
//making sure that the ragdoll is at the correct place.
matchPhysicObjectToBone(link, position, tmpRot1);
}
} }
assert vars.unlock();
for (Bone bone : skeleton.getRoots()) { for (Bone bone : skeleton.getRoots()) {
setUserControl(bone, ragdollEnabled); setUserControl(bone, mode == Mode.Ragdoll);
}
}
/**
* Set the control into Kinematic mode
* In theis mode, the collision shapes follow the movements of the skeleton,
* and can interact with physical environement
*/
public void setKinematicMode() {
if (mode != Mode.Kinetmatic) {
setMode(Mode.Kinetmatic);
}
}
/**
* Sets the control into Ragdoll mode
* The skeleton is entirely controlled by physics.
*/
public void setRagdollMode() {
if (mode != Mode.Ragdoll) {
setMode(Mode.Ragdoll);
} }
} }
/** /**
* Blend motion between the ragdoll actual physic state to the given animation, in the given blendTime * Smoothly blend from Ragdoll mode to Kinematic mode
* Note that this disable the ragdoll behaviour of the control * This is useful to blend ragdoll actual position to a keyframe animation for example
* @param anim the animation name to blend to
* @param channel the channel to use for this animation
* @param blendTime the blending time between ragdoll to anim. * @param blendTime the blending time between ragdoll to anim.
*/ */
public void blendRagdollToAnim(String anim, AnimChannel channel, float blendTime) { public void blendToKinematicMode(float blendTime) {
if (mode == Mode.Kinetmatic) {
return;
}
blendedControl = true; blendedControl = true;
this.blendTime = blendTime; this.blendTime = blendTime;
control = false; mode = Mode.Kinetmatic;
AnimControl animControl = targetModel.getControl(AnimControl.class); AnimControl animControl = targetModel.getControl(AnimControl.class);
animControl.setEnabled(true); animControl.setEnabled(true);
channel.setAnim(anim);
channel.setLoopMode(LoopMode.DontLoop);
TempVars vars = TempVars.get(); TempVars vars = TempVars.get();
assert vars.lock(); assert vars.lock();
//this.control = control;
for (PhysicsBoneLink link : boneLinks.values()) { for (PhysicsBoneLink link : boneLinks.values()) {
Vector3f p = link.rigidBody.getMotionState().getWorldLocation(); Vector3f p = link.rigidBody.getMotionState().getWorldLocation();
@ -842,12 +897,11 @@ public class RagdollControl implements PhysicsControl, PhysicsCollisionListener
} }
/** /**
* returns true if the ragdoll behaviour is enabled * retruns the mode of this control
* @return * @return
*/ */
public boolean isRagdollEnabled() { public Mode getMode() {
return control; return mode;
} }
/** /**

@ -39,12 +39,13 @@ import com.jme3.animation.LoopMode;
import com.jme3.bullet.BulletAppState; import com.jme3.bullet.BulletAppState;
import com.jme3.app.SimpleApplication; import com.jme3.app.SimpleApplication;
import com.jme3.asset.TextureKey; import com.jme3.asset.TextureKey;
import com.jme3.bounding.BoundingBox;
import com.jme3.bullet.PhysicsSpace; import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.PhysicsCollisionEvent; import com.jme3.bullet.collision.PhysicsCollisionEvent;
import com.jme3.bullet.collision.PhysicsCollisionObject; import com.jme3.bullet.collision.PhysicsCollisionObject;
import com.jme3.bullet.collision.RagdollCollisionListener; import com.jme3.bullet.collision.RagdollCollisionListener;
import com.jme3.bullet.collision.shapes.SphereCollisionShape; import com.jme3.bullet.collision.shapes.SphereCollisionShape;
import com.jme3.bullet.control.RagdollControl; import com.jme3.bullet.control.KinematicRagdollControl;
import com.jme3.bullet.control.RigidBodyControl; import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.font.BitmapText; import com.jme3.font.BitmapText;
import com.jme3.input.KeyInput; import com.jme3.input.KeyInput;
@ -74,7 +75,7 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
private BulletAppState bulletAppState; private BulletAppState bulletAppState;
Material matBullet; Material matBullet;
Node model; Node model;
RagdollControl ragdoll; KinematicRagdollControl ragdoll;
float bulletSize = 1f; float bulletSize = 1f;
Material mat; Material mat;
Material mat3; Material mat3;
@ -106,7 +107,7 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
setupLight(); setupLight();
model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml"); model = (Node) assetManager.loadModel("Models/Sinbad/Sinbad.mesh.xml");
// model.setLocalTranslation(5, 0, 5);
// model.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X)); // model.setLocalRotation(new Quaternion().fromAngleAxis(FastMath.HALF_PI, Vector3f.UNIT_X));
//debug view //debug view
@ -119,7 +120,7 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
skeletonDebug.setLocalTranslation(model.getLocalTranslation()); skeletonDebug.setLocalTranslation(model.getLocalTranslation());
//Note: PhysicsRagdollControl is still TODO, constructor will change //Note: PhysicsRagdollControl is still TODO, constructor will change
ragdoll = new RagdollControl(0.5f); ragdoll = new KinematicRagdollControl(0.5f);
setupSinbad(ragdoll); setupSinbad(ragdoll);
ragdoll.addCollisionListener(this); ragdoll.addCollisionListener(this);
model.addControl(ragdoll); model.addControl(ragdoll);
@ -157,12 +158,16 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
float[] angles = new float[3]; float[] angles = new float[3];
model.getLocalRotation().toAngles(angles); model.getLocalRotation().toAngles(angles);
q.fromAngleAxis(angles[1], Vector3f.UNIT_Y); q.fromAngleAxis(angles[1], Vector3f.UNIT_Y);
//q.lookAt(model.getLocalRotation().toRotationMatrix().getColumn(2), Vector3f.UNIT_Y);
model.setLocalRotation(q); model.setLocalRotation(q);
if (angles[0] < 0) { if (angles[0] < 0) {
ragdoll.blendRagdollToAnim("StandUpBack", animChannel, 1); animChannel.setAnim("StandUpBack");
// ragdoll.setKinematicMode();
ragdoll.blendToKinematicMode(0.5f);
} else { } else {
ragdoll.blendRagdollToAnim("StandUpFront", animChannel, 1); animChannel.setAnim("StandUpFront");
ragdoll.blendToKinematicMode(0.5f);
// ragdoll.setKinematicMode();
} }
} }
@ -178,8 +183,10 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
// bulletSize = 0; // bulletSize = 0;
} }
if (name.equals("stop") && isPressed) { if (name.equals("stop") && isPressed) {
bulletAppState.setEnabled(!bulletAppState.isEnabled()); // bulletAppState.setEnabled(!bulletAppState.isEnabled());
ragdoll.setEnabled(!ragdoll.isEnabled());
ragdoll.setRagdollMode();
} }
if (name.equals("shoot") && !isPressed) { if (name.equals("shoot") && !isPressed) {
Geometry bulletg = new Geometry("bullet", bullet); Geometry bulletg = new Geometry("bullet", bullet);
@ -265,13 +272,11 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
} }
} }
if (!ragdoll.isRagdollEnabled()) { ragdoll.setRagdollMode();
ragdoll.setRagdollEnabled(true);
}
} }
private void setupSinbad(RagdollControl ragdoll) { private void setupSinbad(KinematicRagdollControl ragdoll) {
ragdoll.addBoneName("Ulna.L"); ragdoll.addBoneName("Ulna.L");
ragdoll.addBoneName("Ulna.R"); ragdoll.addBoneName("Ulna.R");
ragdoll.addBoneName("Chest"); ragdoll.addBoneName("Chest");
@ -304,6 +309,7 @@ public class TestBoneRagdoll extends SimpleApplication implements RagdollCollisi
@Override @Override
public void simpleUpdate(float tpf) { public void simpleUpdate(float tpf) {
System.out.println(((BoundingBox) model.getWorldBound()).getYExtent());
// elTime += tpf; // elTime += tpf;
// if (elTime > 3) { // if (elTime > 3) {
// elTime = 0; // elTime = 0;

Loading…
Cancel
Save