--- /dev/null
+package com.badlogic.gdx.graphics.g3d.utils;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.graphics.g3d.model.Animation;
+import com.badlogic.gdx.graphics.g3d.model.Node;
+import com.badlogic.gdx.graphics.g3d.model.NodeAnimation;
+import com.badlogic.gdx.graphics.g3d.model.NodeKeyframe;
+import com.badlogic.gdx.math.Matrix4;
+import com.badlogic.gdx.math.Quaternion;
+import com.badlogic.gdx.math.Vector3;
+import com.badlogic.gdx.utils.GdxRuntimeException;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.badlogic.gdx.utils.Pool;
+import com.badlogic.gdx.utils.Pool.Poolable;
+
+public class AnimationController extends BaseAnimationController {
+ public interface AnimationListener {
+ void onEnd(final AnimationDesc animation);
+ void onLoop(final AnimationDesc animation);
+ }
+ public static class AnimationDesc {
+ /** Listener which will be informed when the animation is looped or ended. */
+ public AnimationListener listener;
+ /** The animation to be applied. */
+ public Animation animation;
+ /** The speed at which to play the animation (can be negative), 1.0 for normal speed. */
+ public float speed;
+ /** The current animation time. */
+ public float time;
+ /** The number of remaining loops, negative for continuous, zero if stopped. */
+ public int loopCount;
+ /** @return the remaining time or 0 if still animating. */
+ public float update(float delta) {
+ if (loopCount != 0 && animation != null) {
+ final float duration = animation.duration;
+ final float diff = speed * delta;
+ time += diff;
+ int loops = (int)Math.abs(time / duration);
+ if (time < 0f) {
+ loops++;
+ while (time < 0f)
+ time += duration;
+ }
+ time = Math.abs(time % duration);
+ for (int i = 0; i < loops; i++) {
+ if (loopCount > 0)
+ loopCount--;
+ if (listener != null)
+ listener.onLoop(this);
+ if (loopCount == 0) {
+ final float result = ((loops - 1) - i) * duration + (diff < 0f ? duration - time : time);
+ time = (diff < 0f) ? duration : 0f;
+ if (listener != null)
+ listener.onEnd(this);
+ return result;
+ }
+ }
+ return 0f;
+ } else
+ return delta;
+ }
+ }
+ protected final Pool<AnimationDesc> animationPool = new Pool<AnimationDesc>() {
+ @Override
+ protected AnimationDesc newObject() {
+ return new AnimationDesc();
+ }
+ };
+
+ public AnimationDesc current;
+ public AnimationDesc queued;
+ public float queuedTransitionTime;
+ public AnimationDesc previous;
+ public float transitionCurrentTime;
+ public float transitionTargetTime;
+ public boolean inAction;
+
+ public AnimationController (ModelInstance target) {
+ super(target);
+ }
+
+ private AnimationDesc obtain(final Animation anim, int loopCount, float speed, final AnimationListener listener) {
+ final AnimationDesc result = animationPool.obtain();
+ result.animation = anim;
+ result.listener = listener;
+ result.loopCount = loopCount;
+ result.speed = speed;
+ result.time = speed < 0 ? anim.duration : 0.f;
+ return result;
+ }
+
+ private AnimationDesc obtain(final String id, int loopCount, float speed, final AnimationListener listener) {
+ final Animation anim = target.getAnimation(id);
+ if (anim == null)
+ throw new GdxRuntimeException("Unknown animation: "+id);
+ return obtain(anim, loopCount, speed, listener);
+ }
+
+ private AnimationDesc obtain(final AnimationDesc anim) {
+ return obtain(anim.animation, anim.loopCount, anim.speed, anim.listener);
+ }
+
+ private boolean updating; //FIXME
+ /** @param delta The time elapsed since last update, change this to alter the overall speed (can be negative). */
+ public void update(float delta) {
+ if (current == null || current.loopCount == 0 || current.animation == null)
+ return;
+ updating = true;
+ final float remain = current.update(delta);
+ if (remain != 0f && queued != null) {
+ inAction = false;
+ animate(queued, queuedTransitionTime);
+ queued = null;
+ updating = false;
+ update(remain);
+ return;
+ }
+ if (previous != null && ((transitionCurrentTime += delta) >= transitionTargetTime)) {
+ animationPool.free(previous);
+ previous = null;
+ }
+ if (previous != null)
+ applyAnimations(previous.animation, previous.time, current.animation, current.time, transitionCurrentTime / transitionTargetTime);
+ else
+ applyAnimation(current.animation, current.time);
+ updating = false;
+ }
+
+ /** Set the active animation, replacing any current animation. */
+ public void setAnimation(final String id, int loopCount, float speed, final AnimationListener listener) {
+ setAnimation(obtain(id, loopCount, speed, listener));
+ }
+
+ /** Set the active animation, replacing any current animation. */
+ protected void setAnimation(final Animation anim, int loopCount, float speed, final AnimationListener listener) {
+ setAnimation(obtain(anim, loopCount, speed, listener));
+ }
+
+ /** Set the active animation, replacing any current animation. */
+ protected void setAnimation(final AnimationDesc anim) {
+ if (updating) // FIXME Remove this? Just intended for debugging
+ throw new GdxRuntimeException("Cannot change animation during update");
+ if (current == null)
+ current = anim;
+ else {
+ if (current.animation == anim.animation)
+ anim.time = current.time;
+ animationPool.free(current);
+ current = anim;
+ }
+ }
+
+ /** Changes the current animation by blending the new on top of the old during the transition time. */
+ public void animate(final String id, int loopCount, float speed, final AnimationListener listener, float transitionTime) {
+ animate(obtain(id, loopCount, speed, listener), transitionTime);
+ }
+
+ /** Changes the current animation by blending the new on top of the old during the transition time. */
+ protected void animate(final Animation anim, int loopCount, float speed, final AnimationListener listener, float transitionTime) {
+ animate(obtain(anim, loopCount, speed, listener), transitionTime);
+ }
+
+ /** Changes the current animation by blending the new on top of the old during the transition time. */
+ protected void animate(final AnimationDesc anim, float transitionTime) {
+ if (current == null)
+ current = anim;
+ else if (inAction)
+ queue(anim, transitionTime);
+ else if (current.animation == anim.animation) {
+ anim.time = current.time;
+ animationPool.free(current);
+ current = anim;
+ } else {
+ if (previous != null)
+ animationPool.free(previous);
+ previous = current;
+ current = anim;
+ transitionCurrentTime = 0f;
+ transitionTargetTime = transitionTime;
+ }
+ }
+
+ /** Queue an animation to be applied when the current is finished. If current is continuous it will be synced on next loop. */
+ public void queue(final String id, int loopCount, float speed, final AnimationListener listener, float transitionTime) {
+ queue(obtain(id, loopCount, speed, listener), transitionTime);
+ }
+
+ /** Queue an animation to be applied when the current is finished. If current is continuous it will be synced on next loop. */
+ protected void queue(final Animation anim, int loopCount, float speed, final AnimationListener listener, float transitionTime) {
+ queue(obtain(anim, loopCount, speed, listener), transitionTime);
+ }
+
+ /** Queue an animation to be applied when the current is finished. If current is continuous it will be synced on next loop. */
+ protected void queue(final AnimationDesc anim, float transitionTime) {
+ if (current == null || current.loopCount == 0)
+ animate(anim, transitionTime);
+ else {
+ if (queued != null)
+ animationPool.free(queued);
+ queued = anim;
+ queuedTransitionTime = transitionTime;
+ if (current.loopCount < 0)
+ current.loopCount = 1;
+ }
+ }
+
+ /** Apply an action animation on top of the current animation. */
+ public void action(final String id, int loopCount, float speed, final AnimationListener listener, float transitionTime) {
+ action(obtain(id, loopCount, speed, listener), transitionTime);
+ }
+
+ /** Apply an action animation on top of the current animation. */
+ protected void action(final Animation anim, int loopCount, float speed, final AnimationListener listener, float transitionTime) {
+ action(obtain(anim, loopCount, speed, listener), transitionTime);
+ }
+
+ /** Apply an action animation on top of the current animation. */
+ protected void action(final AnimationDesc anim, float transitionTime) {
+ if (anim.loopCount < 0)
+ throw new GdxRuntimeException("An action cannot be continuous");
+ if (current == null || current.loopCount == 0)
+ animate(anim, transitionTime);
+ else {
+ AnimationDesc toQueue = inAction ? null : obtain(current);
+ inAction = false;
+ animate(anim, transitionTime);
+ inAction = true;
+ if (toQueue != null)
+ queue(toQueue, transitionTime);
+ }
+ }
+}
--- /dev/null
+package com.badlogic.gdx.graphics.g3d.utils;
+
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.graphics.g3d.model.Animation;
+import com.badlogic.gdx.graphics.g3d.model.Node;
+import com.badlogic.gdx.graphics.g3d.model.NodeAnimation;
+import com.badlogic.gdx.graphics.g3d.model.NodeKeyframe;
+import com.badlogic.gdx.math.Matrix4;
+import com.badlogic.gdx.math.Quaternion;
+import com.badlogic.gdx.math.Vector3;
+import com.badlogic.gdx.utils.GdxRuntimeException;
+import com.badlogic.gdx.utils.ObjectMap;
+import com.badlogic.gdx.utils.Pool;
+import com.badlogic.gdx.utils.ObjectMap.Entry;
+import com.badlogic.gdx.utils.Pool.Poolable;
+
+public class BaseAnimationController {
+ public final static class Transform implements Poolable {
+ public final Vector3 translation = new Vector3();
+ public final Quaternion rotation = new Quaternion();
+ public final Vector3 scale = new Vector3(1,1,1);
+ public Transform () { }
+ public Transform idt() {
+ translation.set(0,0,0);
+ rotation.idt();
+ scale.set(1,1,1);
+ return this;
+ }
+ public Transform set(final Vector3 t, final Quaternion r, final Vector3 s) {
+ translation.set(t);
+ rotation.set(r);
+ scale.set(s);
+ return this;
+ }
+ public Transform set(final Transform other) {
+ return set(other.translation, other.rotation, other.scale);
+ }
+ public Transform lerp(final Transform target, final float alpha) {
+ return lerp(target.translation, target.rotation, target.scale, alpha);
+ }
+ public Transform lerp(final Vector3 targetT, final Quaternion targetR, final Vector3 targetS, final float alpha) {
+ translation.lerp(targetT, alpha);
+ rotation.slerp(targetR, alpha);
+ scale.lerp(targetS, alpha);
+ return this;
+ }
+ public Matrix4 toMatrix4(final Matrix4 out) {
+ out.idt();
+ out.translate(translation);
+ out.rotate(rotation);
+ out.scl(scale);
+ return out;
+ }
+ @Override
+ public void reset () {
+ idt();
+ }
+ }
+
+ private final Pool<Transform> transformPool = new Pool<Transform>() {
+ @Override
+ protected Transform newObject () {
+ return new Transform();
+ }
+ };
+ private final static ObjectMap<Node, Transform> transforms = new ObjectMap<Node, Transform>();
+ private boolean applying = false;
+ public final ModelInstance target;
+
+ public BaseAnimationController(final ModelInstance target) {
+ this.target = target;
+ }
+
+ /** Begin applying multiple animations to the instance,
+ * must followed by one or more calls to {{@link #apply(Animation, float, float)} and finally {{@link #end()}. */
+ protected void begin() {
+ if (applying)
+ throw new GdxRuntimeException("You must call end() after each call to being()");
+ applying = true;
+ }
+
+ /** Apply an animation, must be called between {{@link #begin()} and {{@link #end()}.
+ * @param weight The blend weight of this animation relative to the previous applied animations. */
+ protected void apply(final Animation animation, final float time, final float weight) {
+ if (!applying)
+ throw new GdxRuntimeException("You must call begin() before adding an animation");
+ applyAnimation(transforms, transformPool, weight, animation, time);
+ }
+
+ /** End applying multiple animations to the instance and update it to reflect the changes. */
+ protected void end() {
+ if (!applying)
+ throw new GdxRuntimeException("You must call begin() first");
+ for (Entry<Node, Transform> entry : transforms.entries()) {
+ entry.value.toMatrix4(entry.key.localTransform);
+ transformPool.free(entry.value);
+ }
+ transforms.clear();
+ target.calculateTransforms();
+ applying = false;
+ }
+
+ /** Apply a single animation to the {@link ModelInstance} and update the it to reflect the changes. */
+ protected void applyAnimation(final Animation animation, final float time) {
+ if (applying)
+ throw new GdxRuntimeException("Call end() first");
+ applyAnimation(null, null, 1.f, animation, time);
+ target.calculateTransforms();
+ }
+
+ /** Apply two animations, blending the second onto to first using weight. */
+ protected void applyAnimations(final Animation anim1, final float time1, final Animation anim2, final float time2, final float weight) {
+ if (anim2 == null || weight == 0.f)
+ applyAnimation(anim1, time1);
+ else if (anim1 == null || weight == 1.f)
+ applyAnimation(anim2, time2);
+ else if (applying)
+ throw new GdxRuntimeException("Call end() first");
+ else {
+ begin();
+ apply(anim1, time1, 1.f);
+ apply(anim2, time2, weight);
+ end();
+ }
+ }
+
+ private final static Transform tmpT = new Transform();
+ /** Helper method to apply one animation to either an objectmap for blending or directly to the bones. */
+ protected static void applyAnimation(final ObjectMap<Node, Transform> out, final Pool<Transform> pool, final float alpha, final Animation animation, final float time) {
+ for (final NodeAnimation nodeAnim : animation.nodeAnimations) {
+ final Node node = nodeAnim.node;
+ node.isAnimated = true;
+ // Find the keyframe(s)
+ final int n = nodeAnim.keyframes.size - 1;
+ int first = 0, second = -1;
+ for (int i = 0; i < n; i++) {
+ if (time >= nodeAnim.keyframes.get(i).keytime && time <= nodeAnim.keyframes.get(i+1).keytime) {
+ first = i;
+ second = i+1;
+ break;
+ }
+ }
+ // Apply the first keyframe:
+ final Transform transform = tmpT;
+ final NodeKeyframe firstKeyframe = nodeAnim.keyframes.get(first);
+ transform.set(firstKeyframe.translation, firstKeyframe.rotation, firstKeyframe.scale);
+ // Lerp the second keyframe
+ if (second > first) {
+ final NodeKeyframe secondKeyframe = nodeAnim.keyframes.get(second);
+ final float t = (time - firstKeyframe.keytime) / (secondKeyframe.keytime - firstKeyframe.keytime);
+ transform.lerp(secondKeyframe.translation, secondKeyframe.rotation, secondKeyframe.scale, t);
+ }
+ // Apply the transform, either directly to the bone or to out when blending
+ if (out == null)
+ transform.toMatrix4(node.localTransform);
+ else {
+ if (out.containsKey(node)) {
+ if (alpha == 1.f)
+ out.get(node).set(transform);
+ else
+ out.get(node).lerp(transform, alpha);
+ } else {
+ out.put(node, pool.obtain().set(transform));
+ }
+ }
+ }
+ }
+}
--- /dev/null
+package com.badlogic.gdx.tests.g3d;
+
+import com.badlogic.gdx.Gdx;
+import com.badlogic.gdx.Input.Keys;
+import com.badlogic.gdx.graphics.Color;
+import com.badlogic.gdx.graphics.GL10;
+import com.badlogic.gdx.graphics.Texture;
+import com.badlogic.gdx.graphics.VertexAttributes.Usage;
+import com.badlogic.gdx.graphics.g3d.Model;
+import com.badlogic.gdx.graphics.g3d.ModelBatch;
+import com.badlogic.gdx.graphics.g3d.ModelInstance;
+import com.badlogic.gdx.graphics.g3d.lights.DirectionalLight;
+import com.badlogic.gdx.graphics.g3d.lights.Lights;
+import com.badlogic.gdx.graphics.g3d.materials.ColorAttribute;
+import com.badlogic.gdx.graphics.g3d.materials.Material;
+import com.badlogic.gdx.graphics.g3d.materials.TextureAttribute;
+import com.badlogic.gdx.graphics.g3d.model.Animation;
+import com.badlogic.gdx.graphics.g3d.model.MeshPart;
+import com.badlogic.gdx.graphics.g3d.model.Node;
+import com.badlogic.gdx.graphics.g3d.model.NodeAnimation;
+import com.badlogic.gdx.graphics.g3d.utils.AnimationController;
+import com.badlogic.gdx.graphics.g3d.utils.MeshPartBuilder;
+import com.badlogic.gdx.graphics.g3d.utils.ModelBuilder;
+import com.badlogic.gdx.math.Matrix4;
+import com.badlogic.gdx.math.Quaternion;
+import com.badlogic.gdx.math.Vector3;
+import com.badlogic.gdx.math.collision.BoundingBox;
+import com.badlogic.gdx.utils.Array;
+import com.badlogic.gdx.utils.StringBuilder;
+
+public class Animation3DTest extends BaseG3dHudTest {
+ ModelInstance skydome;
+ Model floorModel;
+ ModelInstance character;
+ AnimationController animation;
+
+ Lights lights = new Lights(0.4f, 0.4f, 0.4f).add(
+ new DirectionalLight().set(0.8f, 0.8f, 0.8f, -1f, -.8f, -.2f)
+ );
+
+ @Override
+ public void create () {
+ super.create();
+ inputController.rotateLeftKey = inputController.rotateRightKey = inputController.forwardKey = inputController.backwardKey = 0;
+ cam.position.set(25, 25, 25);
+ cam.lookAt(0, 0, 0);
+ cam.update();
+ modelsWindow.setVisible(false);
+ assets.load("data/g3d/skydome.g3db", Model.class);
+ assets.load("data/g3d/concrete.png", Texture.class);
+ loading = true;
+ trForward.translation.set(0,0,8f);
+ trBackward.translation.set(0,0,-8f);
+ trLeft.rotation.setFromAxis(Vector3.Y, 90);
+ trRight.rotation.setFromAxis(Vector3.Y, -90);
+
+ ModelBuilder builder = new ModelBuilder();
+ builder.begin();
+ MeshPartBuilder part = builder.part("floor", GL10.GL_TRIANGLES, Usage.Position | Usage.TextureCoordinates | Usage.Normal, new Material());
+ for (float x = -200f; x < 200f; x += 10f) {
+ for (float z = -200f; z < 200f; z += 10f) {
+ part.rect(x, 0, z+10f, x+10f, 0, z+10f, x+10f, 0, z, x, 0, z, 0, 1, 0);
+ }
+ }
+ floorModel = builder.end();
+ }
+
+ final AnimationController.Transform trTmp = new AnimationController.Transform();
+ final AnimationController.Transform trForward = new AnimationController.Transform();
+ final AnimationController.Transform trBackward = new AnimationController.Transform();
+ final AnimationController.Transform trRight = new AnimationController.Transform();
+ final AnimationController.Transform trLeft = new AnimationController.Transform();
+ final Matrix4 tmpMatrix = new Matrix4();
+ int status = 0;
+ final static int idle = 1;
+ final static int walk = 2;
+ final static int back = 3;
+ final static int attack = 4;
+ float angle = 0f;
+ @Override
+ protected void render (ModelBatch batch, Array<ModelInstance> instances) {
+ if (character != null) {
+ animation.update(Gdx.graphics.getDeltaTime());
+ if (upKey) {
+ if (!animation.inAction) {
+ trTmp.idt().lerp(trForward, Gdx.graphics.getDeltaTime() / animation.current.animation.duration);
+ character.transform.mul(trTmp.toMatrix4(tmpMatrix));
+ }
+ if (status != walk) {
+ animation.animate("Walk", -1, 1f, null, 0.2f);
+ status = walk;
+ }
+ } else if (downKey) {
+ if (!animation.inAction) {
+ trTmp.idt().lerp(trBackward, Gdx.graphics.getDeltaTime() / animation.current.animation.duration);
+ character.transform.mul(trTmp.toMatrix4(tmpMatrix));
+ }
+ if (status != back) {
+ animation.animate("Walk", -1, -1f, null, 0.2f);
+ status = back;
+ }
+ } else if (status != idle) {
+ animation.animate("Idle", -1, 1f, null, 0.2f);
+ status = idle;
+ }
+ if (rightKey && (status == walk || status == back) && !animation.inAction) {
+ trTmp.idt().lerp(trRight, Gdx.graphics.getDeltaTime() / animation.current.animation.duration);
+ character.transform.mul(trTmp.toMatrix4(tmpMatrix));
+ } else if (leftKey && (status == walk || status == back) && !animation.inAction) {
+ trTmp.idt().lerp(trLeft, Gdx.graphics.getDeltaTime() / animation.current.animation.duration);
+ character.transform.mul(trTmp.toMatrix4(tmpMatrix));
+ }
+ if (spaceKey && !animation.inAction) {
+ animation.action("Attack", 1, 1f, null, 0.2f);
+ }
+ }
+ batch.render(instances, lights);
+ if (skydome != null)
+ batch.render(skydome);
+ }
+
+ @Override
+ protected void getStatus (StringBuilder stringBuilder) {
+ super.getStatus(stringBuilder);
+ stringBuilder.append(" use arrow keys to walk around, space to attack.");
+ }
+
+ @Override
+ protected void onModelClicked(final String name) { }
+
+ @Override
+ protected void onLoaded() {
+ if (skydome == null) {
+ skydome = new ModelInstance(assets.get("data/g3d/skydome.g3db", Model.class));
+ floorModel.materials.get(0).set(TextureAttribute.createDiffuse(assets.get("data/g3d/concrete.png", Texture.class)));
+ instances.add(new ModelInstance(floorModel));
+ assets.load("data/g3d/knight.g3db", Model.class);
+ loading = true;
+ }
+ else if (character == null) {
+ character = new ModelInstance(assets.get("data/g3d/knight.g3db", Model.class));
+ BoundingBox bbox = new BoundingBox();
+ character.calculateBoundingBox(bbox);
+ character.transform.setToRotation(Vector3.Y, 180).trn(0, -bbox.min.y, 0);
+ instances.add(character);
+ animation = new AnimationController(character);
+ animation.animate("Idle", -1, 1f, null, 0.2f);
+ status = idle;
+ for (Animation anim : character.animations)
+ Gdx.app.log("Test", anim.id);
+ }
+ }
+
+ @Override
+ public boolean needsGL20 () {
+ return true;
+ }
+
+ boolean rightKey, leftKey, upKey, downKey, spaceKey;
+ @Override
+ public boolean keyUp (int keycode) {
+ if (keycode == Keys.LEFT)
+ leftKey = false;
+ if (keycode == Keys.RIGHT)
+ rightKey = false;
+ if (keycode == Keys.UP)
+ upKey = false;
+ if (keycode == Keys.DOWN)
+ downKey = false;
+ if (keycode == Keys.SPACE)
+ spaceKey = false;
+ return super.keyUp(keycode);
+ }
+
+ @Override
+ public boolean keyDown (int keycode) {
+ if (keycode == Keys.LEFT)
+ leftKey = true;
+ if (keycode == Keys.RIGHT)
+ rightKey = true;
+ if (keycode == Keys.UP)
+ upKey = true;
+ if (keycode == Keys.DOWN)
+ downKey = true;
+ if (keycode == Keys.SPACE)
+ spaceKey = true;
+ return super.keyDown(keycode);
+ }
+
+ @Override
+ public void dispose () {
+ super.dispose();
+ floorModel.dispose();
+ }
+}
\ No newline at end of file
import com.badlogic.gdx.tests.*;\r
import com.badlogic.gdx.tests.bench.TiledMapBench;\r
import com.badlogic.gdx.tests.examples.MoveSpriteExample;\r
+import com.badlogic.gdx.tests.g3d.Animation3DTest;\r
import com.badlogic.gdx.tests.g3d.Basic3DTest;\r
import com.badlogic.gdx.tests.g3d.MaterialTest;\r
import com.badlogic.gdx.tests.g3d.ModelTest;\r
TextButtonTest.class, TextButtonTestGL2.class, TextureBindTest.class, SortedSpriteTest.class,\r
ExternalMusicTest.class, SoftKeyboardTest.class, DirtyRenderingTest.class, YDownTest.class,\r
ScreenCaptureTest.class, BitmapFontTest.class, LabelScaleTest.class, GamepadTest.class, NetAPITest.class, TideMapAssetManagerTest.class, TideMapDirectLoaderTest.class, TiledMapAssetManagerTest.class, TiledMapBench.class,\r
- RunnablePostTest.class, Vector2dTest.class, SuperKoalio.class, NinePatchTest.class, Basic3DSceneTest.class,\r
+ RunnablePostTest.class, Vector2dTest.class, SuperKoalio.class, NinePatchTest.class, Basic3DSceneTest.class, Animation3DTest.class,\r
ModelTest.class, Basic3DTest.class, ShaderTest.class, SkeletonTest.class, HexagonalTiledMapTest.class));\r
\r
public static List<String> getNames () {\r